From e1ddb95250357a4f1eb63386a71ce24ba00562e1 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Fri, 24 Mar 2023 18:21:25 +0100 Subject: [PATCH 001/208] Inital passportjs integration --- package-lock.json | 433 +++++++++++++++++++++- package.json | 7 +- server/Auth.js | 346 ++++++++--------- server/Server.js | 38 +- server/controllers/UserController.js | 4 +- server/objects/settings/ServerSettings.js | 42 ++- 6 files changed, 695 insertions(+), 175 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8edcbc01c..efddfd17c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,14 @@ "dependencies": { "axios": "^0.27.2", "express": "^4.17.1", + "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "node-tone": "^1.0.1", + "passport": "^0.6.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "socket.io": "^4.5.4", "xml2js": "^0.4.23" }, @@ -111,6 +116,14 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -165,6 +178,11 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -357,6 +375,14 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -492,6 +518,32 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -754,6 +806,75 @@ "node": ">=0.12.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/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==" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -902,6 +1023,11 @@ "node": ">=0.10.0" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -929,6 +1055,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -937,11 +1071,91 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", + "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -986,6 +1200,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1278,6 +1500,22 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1347,6 +1585,11 @@ "engines": { "node": ">=4.0" } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } }, "dependencies": { @@ -1428,6 +1671,11 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1472,6 +1720,11 @@ "fill-range": "^7.0.1" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1604,6 +1857,14 @@ "domhandler": "^5.0.1" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1709,6 +1970,28 @@ "vary": "~1.1.2" } }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + } + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1889,6 +2172,64 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1996,6 +2337,11 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2014,16 +2360,78 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "requires": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-oauth2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", + "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", + "requires": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2053,6 +2461,11 @@ "side-channel": "^1.0.4" } }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2272,6 +2685,19 @@ "mime-types": "~2.1.24" } }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, + "uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -2312,6 +2738,11 @@ "version": "11.0.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index a8b1c69b4..0cf19f48f 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,18 @@ "dependencies": { "axios": "^0.27.2", "express": "^4.17.1", + "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "node-tone": "^1.0.1", + "passport": "^0.6.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "socket.io": "^4.5.4", "xml2js": "^0.4.23" }, "devDependencies": { "nodemon": "^2.0.20" } -} \ No newline at end of file +} diff --git a/server/Auth.js b/server/Auth.js index 2bca48d2b..4cb40da66 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,43 +1,144 @@ +const passport = require('passport') const bcrypt = require('./libs/bcryptjs') const jwt = require('./libs/jsonwebtoken') -const requestIp = require('./libs/requestIp') -const Logger = require('./Logger') +const LocalStrategy = require('passport-local') +const JwtStrategy = require('passport-jwt').Strategy; +const ExtractJwt = require('passport-jwt').ExtractJwt; +const GoogleStrategy = require('passport-google-oauth20').Strategy; +const User = require('./objects/user/User.js') +/** + * @class Class for handling all the authentication related functionality. + */ class Auth { + constructor(db) { this.db = db - - this.user = null } - get username() { - return this.user ? this.user.username : 'nobody' + /** + * Inializes all passportjs stragegies and other passportjs ralated initialization. + */ + initPassportJs() { + // Check if we should load the local strategy + if (global.ServerSettings.authActiveAuthMethods.includes("local")) { + passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) + } + // Check if we should load the google-oauth20 strategy + if (global.ServerSettings.authActiveAuthMethods.includes("google-oauth20")) { + passport.use(new GoogleStrategy({ + clientID: global.ServerSettings.authGoogleOauth20ClientID, + clientSecret: global.ServerSettings.authGoogleOauth20ClientSecret, + callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL + }, function (accessToken, refreshToken, profile, done) { + // TODO: what to use as username + // TODO: do we want to create the users which does not exist? + return done(null, { username: profile.emails[0].value }) + })) + } + + // Load the JwtStrategy (always) -> for bearer token auth + passport.use(new JwtStrategy({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: global.ServerSettings.tokenSecret + }, this.jwtAuthCheck.bind(this))) + + // define how to seralize a user (to be put into the session) + passport.serializeUser(function (user, cb) { + process.nextTick(function () { + // only store username and id to session + // TODO: do we want to store more info in the session? + return cb(null, { + "username": user.username, + "id": user.id, + }); + }); + }); + + // define how to deseralize a user (use the username to get it from the database) + passport.deserializeUser(function (user, cb) { + process.nextTick(function () { + parsedUserInfo = JSON.parse(user) + // TODO: do the matching on username or better on id? + var dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) + return cb(null, new User(dbUser)); + }); + }); } - get users() { - return this.db.users + /** + * Creates all (express) routes required for authentication. + * @param {express.Router} router + */ + initAuthRoutes(router) { + // just a route saying "you need to login" where we redirect e.g. after logout + // TODO: replace with a 401? + router.get('/login', function (req, res) { + res.send('please login') + }) + + // Local strategy login route (takes username and password) + router.post('/login', passport.authenticate('local', { + failureRedirect: '/login' + }), + (function (req, res) { + // return the user login response json if the login was successfull + res.json(this.getUserLoginResponsePayload(req.user.username)) + }).bind(this) + ) + + // google-oauth20 strategy login route (this redirects to the google login) + router.get('/auth/google', passport.authenticate('google', { scope: ['email'] })) + + // google-oauth20 strategy callback route (this receives the token from google) + router.get('/auth/google/callback', + passport.authenticate('google', { failureRedirect: '/login' }), + (function (req, res) { + // return the user login response json if the login was successfull + res.json(this.getUserLoginResponsePayload(req.user.username)) + }).bind(this) + ) + + // Logout route + router.get('/logout', function (req, res) { + // TODO: invalidate possible JWTs + req.logout() + res.redirect('/login') + }) } - cors(req, res, next) { - res.header('Access-Control-Allow-Origin', '*') - res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS') - res.header('Access-Control-Allow-Headers', '*') - // TODO: Make sure allowing all headers is not a security concern. It is required for adding custom headers for SSO - // res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Range, Authorization") - res.header('Access-Control-Allow-Credentials', true) - if (req.method === 'OPTIONS') { - res.sendStatus(200) - } else { + /** + * middleware to use in express to only allow authenticated users. + * @param {express.Request} req + * @param {express.Response} res + * @param {express.NextFunction} next + */ + isAuthenticated(req, res, next) { + // check if session cookie says that we are authenticated + if (req.isAuthenticated()) { next() + } else { + // try JWT to authenticate + passport.authenticate("jwt")(req, res, next) } } + /** + * Function to generate a jwt token for a given user. + * @param {Object} user + * @returns the token. + */ + generateAccessToken(user) { + return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret); + } + + /** + * Generate a token for each user. + */ async initTokenSecret() { if (process.env.TOKEN_SECRET) { // User can supply their own token secret - Logger.debug(`[Auth] Setting token secret - using user passed in TOKEN_SECRET env var`) this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET } else { - Logger.debug(`[Auth] Setting token secret - using random bytes`) this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') } await this.db.updateServerSettings() @@ -46,46 +147,70 @@ class Auth { if (this.db.users.length) { for (const user of this.db.users) { user.token = await this.generateAccessToken({ userId: user.id, username: user.username }) - Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`) } await this.db.updateEntities('user', this.db.users) } } - async authMiddleware(req, res, next) { - var token = null + /** + * Checks if the user in the validated jwt_payload really exists and is active. + * @param {Object} jwt_payload + * @param {function} done + */ + jwtAuthCheck(jwt_payload, done) { + var user = this.db.users.find(u => u.username.toLowerCase() === jwt_payload.username.toLowerCase()) - // If using a get request, the token can be passed as a query string - if (req.method === 'GET' && req.query && req.query.token) { - token = req.query.token - } else { - const authHeader = req.headers['authorization'] - token = authHeader && authHeader.split(' ')[1] + if (!user || !user.isActive) { + done(null, null) + return } - - if (token == null) { - Logger.error('Api called without a token', req.path) - return res.sendStatus(401) - } - - var user = await this.verifyToken(token) - if (!user) { - Logger.error('Verify Token User Not Found', token) - return res.sendStatus(404) - } - if (!user.isActive) { - Logger.error('Verify Token User is disabled', token, user.username) - return res.sendStatus(403) - } - req.user = user - next() + done(null, user) + return } + /** + * Checks if a username and passpword touple is valid and the user active. + * @param {string} username + * @param {string} password + * @param {function} done + */ + localAuthCheckUserPw(username, password, done) { + var user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) + + if (!user || !user.isActive) { + done(null, null) + return + } + + // Check passwordless root user + if (user.id === 'root' && (!user.pash || user.pash === '')) { + if (password) { + done(null, null) + return + } + done(null, user) + return + } + + // Check password match + var compare = bcrypt.compareSync(password, user.pash) + if (compare) { + done(null, user) + return + } + done(null, null) + return + } + + /** + * Hashes a password with bcrypt. + * @param {string} password + * @returns {string} hash + */ hashPass(password) { return new Promise((resolve) => { bcrypt.hash(password, 8, (err, hash) => { if (err) { - Logger.error('Hash failed', err) resolve(null) } else { resolve(hash) @@ -94,28 +219,14 @@ class Auth { }) } - generateAccessToken(payload) { - return jwt.sign(payload, global.ServerSettings.tokenSecret); - } - - authenticateUser(token) { - return this.verifyToken(token) - } - - verifyToken(token) { - return new Promise((resolve) => { - jwt.verify(token, global.ServerSettings.tokenSecret, (err, payload) => { - if (!payload || err) { - Logger.error('JWT Verify Token Failed', err) - return resolve(null) - } - const user = this.users.find(u => u.id === payload.userId && u.username === payload.username) - resolve(user || null) - }) - }) - } - - getUserLoginResponsePayload(user) { + /** + * Return the login info payload for a user. + * @param {string} username + * @returns {string} jsonPayload + */ + getUserLoginResponsePayload(username) { + var user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) + user = new User(user) return { user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries), @@ -123,101 +234,6 @@ class Auth { Source: global.Source } } - - async login(req, res) { - const ipAddress = requestIp.getClientIp(req) - var username = (req.body.username || '').toLowerCase() - var password = req.body.password || '' - - var user = this.users.find(u => u.username.toLowerCase() === username) - - if (!user || !user.isActive) { - Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) - if (req.rateLimit.remaining <= 2) { - Logger.error(`[Auth] Failed login attempt for username ${username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`) - return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`) - } - return res.status(401).send('Invalid user or password') - } - - // Check passwordless root user - if (user.id === 'root' && (!user.pash || user.pash === '')) { - if (password) { - return res.status(401).send('Invalid root password (hint: there is none)') - } else { - return res.json(this.getUserLoginResponsePayload(user)) - } - } - - // Check password match - var compare = await bcrypt.compare(password, user.pash) - if (compare) { - res.json(this.getUserLoginResponsePayload(user)) - } else { - Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) - if (req.rateLimit.remaining <= 2) { - Logger.error(`[Auth] Failed login attempt for user ${user.username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`) - return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`) - } - return res.status(401).send('Invalid user or password') - } - } - - // Not in use now - lockUser(user) { - user.isLocked = true - return this.db.updateEntity('user', user).catch((error) => { - Logger.error('[Auth] Failed to lock user', user.username, error) - return false - }) - } - - comparePassword(password, user) { - if (user.type === 'root' && !password && !user.pash) return true - if (!password || !user.pash) return false - return bcrypt.compare(password, user.pash) - } - - async userChangePassword(req, res) { - var { password, newPassword } = req.body - newPassword = newPassword || '' - var matchingUser = this.users.find(u => u.id === req.user.id) - - // Only root can have an empty password - if (matchingUser.type !== 'root' && !newPassword) { - return res.json({ - error: 'Invalid new password - Only root can have an empty password' - }) - } - - var compare = await this.comparePassword(password, matchingUser) - if (!compare) { - return res.json({ - error: 'Invalid password' - }) - } - - var pw = '' - if (newPassword) { - pw = await this.hashPass(newPassword) - if (!pw) { - return res.json({ - error: 'Hash failed' - }) - } - } - - matchingUser.pash = pw - var success = await this.db.updateEntity('user', matchingUser) - if (success) { - res.json({ - success: true - }) - } else { - res.json({ - error: 'Unknown error' - }) - } - } } + module.exports = Auth \ No newline at end of file diff --git a/server/Server.js b/server/Server.js index a5a0b8b58..17cd62564 100644 --- a/server/Server.js +++ b/server/Server.js @@ -37,6 +37,11 @@ const CronManager = require('./managers/CronManager') const TaskManager = require('./managers/TaskManager') const EBookManager = require('./managers/EBookManager') +//Import the main Passport and Express-Session library +const passport = require('passport') +const expressSession = require('express-session') + + class Server { constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { this.Port = PORT @@ -48,7 +53,7 @@ class Server { global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH)) global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH)) global.RouterBasePath = ROUTER_BASE_PATH - global.XAccel = process.env.USE_X_ACCEL + global.XAccel = process.env.USE_X_ACCELAuth if (!fs.pathExistsSync(global.ConfigPath)) { fs.mkdirSync(global.ConfigPath) @@ -92,7 +97,7 @@ class Server { } authMiddleware(req, res, next) { - this.auth.authMiddleware(req, res, next) + this.auth.isAuthenticated(req, res, next) } async init() { @@ -141,13 +146,33 @@ class Server { await this.init() const app = express() + + // enable express-session + app.use(expressSession({ + secret: global.ServerSettings.tokenSecret, + resave: false, + saveUninitialized: false, + cookie: { + // also send the cookie if were hare not on https + secure: false + }, + })) + // init passport.js + app.use(passport.initialize()) + // register passport in express-session + app.use(passport.session()) + // config passport.js + this.auth.initPassportJs() + // use auth on all routes - not used now + // app.use(passport.authenticate('session')); + const router = express.Router() app.use(global.RouterBasePath, router) app.disable('x-powered-by') this.server = http.createServer(app) - router.use(this.auth.cors) + // router.use(this.auth.cors) router.use(fileUpload()) router.use(express.urlencoded({ extended: true, limit: "5mb" })); router.use(express.json({ limit: "5mb" })) @@ -166,6 +191,9 @@ class Server { router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router) router.use('/s', this.authMiddleware.bind(this), this.staticRouter.router) + // Auth routes + this.auth.initAuthRoutes(router) + // EBook static file routes router.get('/ebook/:library/:folder/*', (req, res) => { const library = this.db.libraries.find(lib => lib.id === req.params.library) @@ -213,8 +241,8 @@ class Server { ] dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html')))) - router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res)) - router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) + // router.post('/login', passport.authenticate('local', this.auth.login), this.auth.loginResult.bind(this)) + // router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) router.post('/init', (req, res) => { if (this.db.hasRootUser) { Logger.error(`[Server] attempt to init server when server already has a root user`) diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index af0ce0e19..6e39f6f42 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -43,7 +43,7 @@ class UserController { account.id = getId('usr') account.pash = await this.auth.hashPass(account.password) delete account.password - account.token = await this.auth.generateAccessToken({ userId: account.id, username }) + account.token = await this.auth.generateAccessToken(account) account.createdAt = Date.now() var newUser = new User(account) var success = await this.db.insertEntity('user', newUser) @@ -85,7 +85,7 @@ class UserController { var hasUpdated = user.update(account) if (hasUpdated) { if (shouldUpdateToken) { - user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username }) + user.token = await this.auth.generateAccessToken(user) Logger.info(`[UserController] User ${user.username} was generated a new api token`) } await this.db.updateEntity('user', user) diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 38b753706..9bdc77967 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -57,6 +57,16 @@ class ServerSettings { this.version = null + // Auth settings + // Active auth methodes + this.authActiveAuthMethods = ['local'] + + // google-oauth20 settings + this.authGoogleOauth20ClientID = '' + this.authGoogleOauth20ClientSecret = '' + this.authGoogleOauth20CallbackURL = '' + + if (settings) { this.construct(settings) } @@ -100,6 +110,30 @@ class ServerSettings { this.logLevel = settings.logLevel || Logger.logLevel this.version = settings.version || null + this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] + this.authGoogleOauth20ClientID = settings.authGoogleOauth20ClientID || '' + this.authGoogleOauth20ClientSecret = settings.authGoogleOauth20ClientSecret || '' + this.authGoogleOauth20CallbackURL = settings.authGoogleOauth20CallbackURL || '' + + if (!Array.isArray(this.authActiveAuthMethods)) { + this.authActiveAuthMethods = ['local'] + } + + // remove uninitialized methods + // GoogleOauth20 + if (this.authActiveAuthMethods.includes('google-oauth20') && ( + this.authGoogleOauth20ClientID === '' || + this.authGoogleOauth20ClientSecret === '' || + this.authGoogleOauth20CallbackURL === '' + )) { + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1); + } + + // fallback to local + if (!Array.isArray(this.authActiveAuthMethods) || this.authActiveAuthMethods.length == 0) { + this.authActiveAuthMethods = ['local'] + } + // Migrations if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was renamed to storeCoverWithItem in 2.0.0 this.storeCoverWithItem = !!settings.storeCoverWithBook @@ -148,13 +182,19 @@ class ServerSettings { dateFormat: this.dateFormat, language: this.language, logLevel: this.logLevel, - version: this.version + version: this.version, + authActiveAuthMethods: this.authActiveAuthMethods, + authGoogleOauth20ClientID: this.authGoogleOauth20ClientID, // Do not return to client + authGoogleOauth20ClientSecret: this.authGoogleOauth20ClientSecret, // Do not return to client + authGoogleOauth20CallbackURL: this.authGoogleOauth20CallbackURL } } toJSONForBrowser() { const json = this.toJSON() delete json.tokenSecret + delete json.authGoogleOauth20ClientID + delete json.authGoogleOauth20ClientSecret return json } From 08676a675ae2e5cc945a008b60f00bee52f249fc Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Fri, 24 Mar 2023 18:31:58 +0100 Subject: [PATCH 002/208] Fix: small problem with this context in Auth.js --- server/Auth.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 4cb40da66..16889163f 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -48,22 +48,22 @@ class Auth { process.nextTick(function () { // only store username and id to session // TODO: do we want to store more info in the session? - return cb(null, { + return cb(null, JSON.stringify({ "username": user.username, "id": user.id, - }); + })); }); }); // define how to deseralize a user (use the username to get it from the database) - passport.deserializeUser(function (user, cb) { - process.nextTick(function () { - parsedUserInfo = JSON.parse(user) + passport.deserializeUser((function (user, cb) { + process.nextTick((function () { + const parsedUserInfo = JSON.parse(user) // TODO: do the matching on username or better on id? var dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) return cb(null, new User(dbUser)); - }); - }); + }).bind(this)); + }).bind(this)); } /** From 62b0940766c11df1e705756dc454a06b9b5117a4 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Fri, 14 Apr 2023 20:26:29 +0200 Subject: [PATCH 003/208] Added passport-openidconnect implementation --- package-lock.json | 26 +++++++++++ package.json | 1 + server/Auth.js | 54 +++++++++++++++++++++-- server/objects/settings/ServerSettings.js | 41 ++++++++++++++++- 4 files changed, 118 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82657abaf..c32ea13bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", + "passport-openidconnect": "^0.1.1", "socket.io": "^4.5.4", "xml2js": "^0.4.23" }, @@ -1138,6 +1139,22 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-openidconnect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", + "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", + "dependencies": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -2417,6 +2434,15 @@ "utils-merge": "1.x.x" } }, + "passport-openidconnect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", + "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", + "requires": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x" + } + }, "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", diff --git a/package.json b/package.json index 3190539a3..95429a084 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", + "passport-openidconnect": "^0.1.1", "socket.io": "^4.5.4", "xml2js": "^0.4.23" }, diff --git a/server/Auth.js b/server/Auth.js index 16889163f..40bb64acd 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -5,6 +5,7 @@ const LocalStrategy = require('passport-local') const JwtStrategy = require('passport-jwt').Strategy; const ExtractJwt = require('passport-jwt').ExtractJwt; const GoogleStrategy = require('passport-google-oauth20').Strategy; +var OpenIDConnectStrategy = require('passport-openidconnect'); const User = require('./objects/user/User.js') /** @@ -24,17 +25,52 @@ class Auth { if (global.ServerSettings.authActiveAuthMethods.includes("local")) { passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) } + // Check if we should load the google-oauth20 strategy if (global.ServerSettings.authActiveAuthMethods.includes("google-oauth20")) { passport.use(new GoogleStrategy({ clientID: global.ServerSettings.authGoogleOauth20ClientID, clientSecret: global.ServerSettings.authGoogleOauth20ClientSecret, callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL - }, function (accessToken, refreshToken, profile, done) { + }, (function (accessToken, refreshToken, profile, done) { // TODO: what to use as username // TODO: do we want to create the users which does not exist? - return done(null, { username: profile.emails[0].value }) - })) + var user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) + + if (!user || !user.isActive) { + done(null, null) + return + } + + return done(null, user) + }).bind(this))) + } + + // Check if we should load the openid strategy + if (global.ServerSettings.authActiveAuthMethods.includes("openid")) { + passport.use(new OpenIDConnectStrategy({ + issuer: global.ServerSettings.authOpenIDIssuerURL, + authorizationURL: global.ServerSettings.authOpenIDAuthorizationURL, + tokenURL: global.ServerSettings.authOpenIDTokenURL, + userInfoURL: global.ServerSettings.authOpenIDUserInfoURL, + clientID: global.ServerSettings.authOpenIDClientID, + clientSecret: global.ServerSettings.authOpenIDClientSecret, + callbackURL: global.ServerSettings.authOpenIDCallbackURL, + scope: ["openid", "email", "profile"], + skipUserProfile: false + }, + (function (issuer, profile, done) { + // TODO: what to use as username + // TODO: do we want to create the users which does not exist? + var user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) + + if (!user || !user.isActive) { + done(null, null) + return + } + + return done(null, user) + }).bind(this))) } // Load the JwtStrategy (always) -> for bearer token auth @@ -99,6 +135,18 @@ class Auth { }).bind(this) ) + // openid strategy login route (this redirects to the configured openid login provider) + router.get('/auth/openid', passport.authenticate('openidconnect')); + + // openid strategy callback route (this receives the token from the configured openid login provider) + router.get('/auth/openid/callback', + passport.authenticate('openidconnect', { failureRedirect: '/login' }), + (function (req, res) { + // return the user login response json if the login was successfull + res.json(this.getUserLoginResponsePayload(req.user.username)) + }).bind(this) + ) + // Logout route router.get('/logout', function (req, res) { // TODO: invalidate possible JWTs diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 53e00e9b5..602d8ac1d 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -67,6 +67,14 @@ class ServerSettings { this.authGoogleOauth20ClientSecret = '' this.authGoogleOauth20CallbackURL = '' + // generic-oauth20 settings + this.authOpenIDIssuerURL = '' + this.authOpenIDAuthorizationURL = '' + this.authOpenIDTokenURL = '' + this.authOpenIDUserInfoURL = '' + this.authOpenIDClientID = '' + this.authOpenIDClientSecret = '' + this.authOpenIDCallbackURL = '' if (settings) { this.construct(settings) @@ -117,6 +125,14 @@ class ServerSettings { this.authGoogleOauth20ClientSecret = settings.authGoogleOauth20ClientSecret || '' this.authGoogleOauth20CallbackURL = settings.authGoogleOauth20CallbackURL || '' + this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || '' + this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || '' + this.authOpenIDTokenURL = settings.authOpenIDTokenURL || '' + this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || '' + this.authOpenIDClientID = settings.authOpenIDClientID || '' + this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' + this.authOpenIDCallbackURL = settings.authOpenIDCallbackURL || '' + if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] } @@ -131,6 +147,20 @@ class ServerSettings { this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1); } + // remove uninitialized methods + // OpenID + if (this.authActiveAuthMethods.includes('generic-oauth20') && ( + this.authOpenIDIssuerURL === '' || + this.authOpenIDAuthorizationURL === '' || + this.authOpenIDTokenURL === '' || + this.authOpenIDUserInfoURL === '' || + this.authOpenIDClientID === '' || + this.authOpenIDClientSecret === '' || + this.authOpenIDCallbackURL === '' + )) { + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('generic-oauth20', 0), 1); + } + // fallback to local if (!Array.isArray(this.authActiveAuthMethods) || this.authActiveAuthMethods.length == 0) { this.authActiveAuthMethods = ['local'] @@ -189,7 +219,14 @@ class ServerSettings { authActiveAuthMethods: this.authActiveAuthMethods, authGoogleOauth20ClientID: this.authGoogleOauth20ClientID, // Do not return to client authGoogleOauth20ClientSecret: this.authGoogleOauth20ClientSecret, // Do not return to client - authGoogleOauth20CallbackURL: this.authGoogleOauth20CallbackURL + authGoogleOauth20CallbackURL: this.authGoogleOauth20CallbackURL, + authOpenIDIssuerURL: this.authOpenIDIssuerURL, + authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, + authOpenIDTokenURL: this.authOpenIDTokenURL, + authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, + authOpenIDClientID: this.authOpenIDClientID, // Do not return to client + authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client + authOpenIDCallbackURL: this.authOpenIDCallbackURL } } @@ -198,6 +235,8 @@ class ServerSettings { delete json.tokenSecret delete json.authGoogleOauth20ClientID delete json.authGoogleOauth20ClientSecret + delete json.authOpenIDClientID + delete json.authOpenIDClientSecret return json } From 7010a13648860288b2be43a230f498499e13f0f4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 16 Apr 2023 10:08:13 -0500 Subject: [PATCH 004/208] Fixes for passport local and allow empty password --- client/pages/login.vue | 9 +- package-lock.json | 20 ---- package.json | 1 - server/Auth.js | 76 +++++++------- server/Server.js | 5 +- server/SocketAuthority.js | 2 +- server/libs/passportLocal/LICENSE | 20 ++++ server/libs/passportLocal/index.js | 20 ++++ server/libs/passportLocal/strategy.js | 119 ++++++++++++++++++++++ server/objects/settings/ServerSettings.js | 9 +- 10 files changed, 206 insertions(+), 75 deletions(-) create mode 100644 server/libs/passportLocal/LICENSE create mode 100644 server/libs/passportLocal/index.js create mode 100644 server/libs/passportLocal/strategy.js diff --git a/client/pages/login.vue b/client/pages/login.vue index a94045ae7..8da68f5e8 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -124,7 +124,7 @@ export default { location.reload() }, - setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) { + setUser({ user, userDefaultLibraryId, serverSettings, Source }) { this.$store.commit('setServerSettings', serverSettings) this.$store.commit('setSource', Source) this.$setServerLanguageCode(serverSettings.language) @@ -143,17 +143,18 @@ export default { this.error = null this.processing = true - var payload = { + const payload = { username: this.username, password: this.password || '' } - var authRes = await this.$axios.$post('/login', payload).catch((error) => { + const authRes = await this.$axios.$post('/login', payload).catch((error) => { console.error('Failed', error.response) if (error.response) this.error = error.response.data else this.error = 'Unknown Error' return false }) - if (authRes && authRes.error) { + console.log('Auth res', authRes) + if (authRes?.error) { this.error = authRes.error } else if (authRes) { this.setUser(authRes) diff --git a/package-lock.json b/package-lock.json index 0bfa07d20..e87a4b5c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", - "passport-local": "^1.0.0", "passport-openidconnect": "^0.1.1", "socket.io": "^4.5.4", "xml2js": "^0.4.23" @@ -1109,17 +1108,6 @@ "passport-strategy": "^1.0.0" } }, - "node_modules/passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", - "dependencies": { - "passport-strategy": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/passport-oauth2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", @@ -2414,14 +2402,6 @@ "passport-strategy": "^1.0.0" } }, - "passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", - "requires": { - "passport-strategy": "1.x.x" - } - }, "passport-oauth2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", diff --git a/package.json b/package.json index b5ee0bd10..43ab49e2f 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", - "passport-local": "^1.0.0", "passport-openidconnect": "^0.1.1", "socket.io": "^4.5.4", "xml2js": "^0.4.23" diff --git a/server/Auth.js b/server/Auth.js index 40bb64acd..9e1b2a12d 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,12 +1,11 @@ const passport = require('passport') const bcrypt = require('./libs/bcryptjs') const jwt = require('./libs/jsonwebtoken') -const LocalStrategy = require('passport-local') -const JwtStrategy = require('passport-jwt').Strategy; -const ExtractJwt = require('passport-jwt').ExtractJwt; -const GoogleStrategy = require('passport-google-oauth20').Strategy; -var OpenIDConnectStrategy = require('passport-openidconnect'); -const User = require('./objects/user/User.js') +const LocalStrategy = require('./libs/passportLocal') +const JwtStrategy = require('passport-jwt').Strategy +const ExtractJwt = require('passport-jwt').ExtractJwt +const GoogleStrategy = require('passport-google-oauth20').Strategy +const OpenIDConnectStrategy = require('passport-openidconnect') /** * @class Class for handling all the authentication related functionality. @@ -35,7 +34,7 @@ class Auth { }, (function (accessToken, refreshToken, profile, done) { // TODO: what to use as username // TODO: do we want to create the users which does not exist? - var user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) + const user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -87,19 +86,19 @@ class Auth { return cb(null, JSON.stringify({ "username": user.username, "id": user.id, - })); - }); - }); + })) + }) + }) // define how to deseralize a user (use the username to get it from the database) passport.deserializeUser((function (user, cb) { process.nextTick((function () { const parsedUserInfo = JSON.parse(user) // TODO: do the matching on username or better on id? - var dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) - return cb(null, new User(dbUser)); - }).bind(this)); - }).bind(this)); + const dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) + return cb(null, dbUser) + }).bind(this)) + }).bind(this)) } /** @@ -107,19 +106,11 @@ class Auth { * @param {express.Router} router */ initAuthRoutes(router) { - // just a route saying "you need to login" where we redirect e.g. after logout - // TODO: replace with a 401? - router.get('/login', function (req, res) { - res.send('please login') - }) - // Local strategy login route (takes username and password) - router.post('/login', passport.authenticate('local', { - failureRedirect: '/login' - }), + router.post('/login', passport.authenticate('local'), (function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user.username)) + res.json(this.getUserLoginResponsePayload(req.user)) }).bind(this) ) @@ -128,30 +119,35 @@ class Auth { // google-oauth20 strategy callback route (this receives the token from google) router.get('/auth/google/callback', - passport.authenticate('google', { failureRedirect: '/login' }), + passport.authenticate('google'), (function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user.username)) + res.json(this.getUserLoginResponsePayload(req.user)) }).bind(this) ) // openid strategy login route (this redirects to the configured openid login provider) - router.get('/auth/openid', passport.authenticate('openidconnect')); + router.get('/auth/openid', passport.authenticate('openidconnect')) // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', - passport.authenticate('openidconnect', { failureRedirect: '/login' }), + passport.authenticate('openidconnect'), (function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user.username)) + res.json(this.getUserLoginResponsePayload(req.user)) }).bind(this) ) // Logout route - router.get('/logout', function (req, res) { + router.post('/logout', (req, res) => { // TODO: invalidate possible JWTs - req.logout() - res.redirect('/login') + req.logout((err) => { + if (err) { + res.sendStatus(500) + } else { + res.sendStatus(200) + } + }) }) } @@ -177,7 +173,7 @@ class Auth { * @returns the token. */ generateAccessToken(user) { - return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret); + return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret) } /** @@ -206,7 +202,7 @@ class Auth { * @param {function} done */ jwtAuthCheck(jwt_payload, done) { - var user = this.db.users.find(u => u.username.toLowerCase() === jwt_payload.username.toLowerCase()) + const user = this.db.users.find(u => u.username.toLowerCase() === jwt_payload.username.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -217,13 +213,13 @@ class Auth { } /** - * Checks if a username and passpword touple is valid and the user active. + * Checks if a username and password tuple is valid and the user active. * @param {string} username * @param {string} password * @param {function} done */ - localAuthCheckUserPw(username, password, done) { - var user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) + async localAuthCheckUserPw(username, password, done) { + const user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -241,7 +237,7 @@ class Auth { } // Check password match - var compare = bcrypt.compareSync(password, user.pash) + const compare = await bcrypt.compare(password, user.pash) if (compare) { done(null, user) return @@ -272,9 +268,7 @@ class Auth { * @param {string} username * @returns {string} jsonPayload */ - getUserLoginResponsePayload(username) { - var user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) - user = new User(user) + getUserLoginResponsePayload(user) { return { user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries), diff --git a/server/Server.js b/server/Server.js index d2865b886..96896a73e 100644 --- a/server/Server.js +++ b/server/Server.js @@ -161,7 +161,7 @@ class Server { // config passport.js this.auth.initPassportJs() // use auth on all routes - not used now - // app.use(passport.authenticate('session')); + // app.use(passport.authenticate('session')) const router = express.Router() app.use(global.RouterBasePath, router) @@ -169,9 +169,8 @@ class Server { this.server = http.createServer(app) - // router.use(this.auth.cors) router.use(fileUpload()) - router.use(express.urlencoded({ extended: true, limit: "5mb" })); + router.use(express.urlencoded({ extended: true, limit: "5mb" })) router.use(express.json({ limit: "5mb" })) // Static path to generated nuxt diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index bf6ea6f7f..a12bca29d 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -140,7 +140,7 @@ class SocketAuthority { // When setting up a socket connection the user needs to be associated with a socket id // for this the client will send a 'auth' event that includes the users API token async authenticateSocket(socket, token) { - const user = await this.Server.auth.authenticateUser(token) + const user = await this.Server.db.users.find(u => u.token === token) if (!user) { Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') diff --git a/server/libs/passportLocal/LICENSE b/server/libs/passportLocal/LICENSE new file mode 100644 index 000000000..d8ebfcf1c --- /dev/null +++ b/server/libs/passportLocal/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2011-2014 Jared Hanson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/server/libs/passportLocal/index.js b/server/libs/passportLocal/index.js new file mode 100644 index 000000000..365d4f65e --- /dev/null +++ b/server/libs/passportLocal/index.js @@ -0,0 +1,20 @@ +// +// modified for audiobookshelf +// Source: https://github.com/jaredhanson/passport-local +// + +/** + * Module dependencies. + */ +var Strategy = require('./strategy'); + + +/** + * Expose `Strategy` directly from package. + */ +exports = module.exports = Strategy; + +/** + * Export constructors. + */ +exports.Strategy = Strategy; diff --git a/server/libs/passportLocal/strategy.js b/server/libs/passportLocal/strategy.js new file mode 100644 index 000000000..671102044 --- /dev/null +++ b/server/libs/passportLocal/strategy.js @@ -0,0 +1,119 @@ +/** + * Module dependencies. + */ +const passport = require('passport-strategy') +const util = require('util') + + +function lookup(obj, field) { + if (!obj) { return null; } + var chain = field.split(']').join('').split('['); + for (var i = 0, len = chain.length; i < len; i++) { + var prop = obj[chain[i]]; + if (typeof (prop) === 'undefined') { return null; } + if (typeof (prop) !== 'object') { return prop; } + obj = prop; + } + return null; +} + +/** + * `Strategy` constructor. + * + * The local authentication strategy authenticates requests based on the + * credentials submitted through an HTML-based login form. + * + * Applications must supply a `verify` callback which accepts `username` and + * `password` credentials, and then calls the `done` callback supplying a + * `user`, which should be set to `false` if the credentials are not valid. + * If an exception occured, `err` should be set. + * + * Optionally, `options` can be used to change the fields in which the + * credentials are found. + * + * Options: + * - `usernameField` field name where the username is found, defaults to _username_ + * - `passwordField` field name where the password is found, defaults to _password_ + * - `passReqToCallback` when `true`, `req` is the first argument to the verify callback (default: `false`) + * + * Examples: + * + * passport.use(new LocalStrategy( + * function(username, password, done) { + * User.findOne({ username: username, password: password }, function (err, user) { + * done(err, user); + * }); + * } + * )); + * + * @param {Object} options + * @param {Function} verify + * @api public + */ +function Strategy(options, verify) { + if (typeof options == 'function') { + verify = options; + options = {}; + } + if (!verify) { throw new TypeError('LocalStrategy requires a verify callback'); } + + this._usernameField = options.usernameField || 'username'; + this._passwordField = options.passwordField || 'password'; + + passport.Strategy.call(this); + this.name = 'local'; + this._verify = verify; + this._passReqToCallback = options.passReqToCallback; +} + +/** + * Inherit from `passport.Strategy`. + */ +util.inherits(Strategy, passport.Strategy); + +/** + * Authenticate request based on the contents of a form submission. + * + * @param {Object} req + * @api protected + */ +Strategy.prototype.authenticate = function (req, options) { + options = options || {}; + var username = lookup(req.body, this._usernameField) + if (username === null) { + lookup(req.query, this._usernameField); + } + + var password = lookup(req.body, this._passwordField) + if (password === null) { + password = lookup(req.query, this._passwordField); + } + + if (username === null || password === null) { + return this.fail({ message: options.badRequestMessage || 'Missing credentials' }, 400); + } + + var self = this; + + function verified(err, user, info) { + if (err) { return self.error(err); } + if (!user) { return self.fail(info); } + self.success(user, info); + } + + try { + if (self._passReqToCallback) { + this._verify(req, username, password, verified); + } else { + this._verify(username, password, verified); + } + } catch (ex) { + return self.error(ex); + } +}; + + +/** + * Expose `Strategy`. + */ +module.exports = Strategy; diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 602d8ac1d..2a51e3de8 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -1,5 +1,4 @@ const { BookshelfView } = require('../../utils/constants') -const { isNullOrNaN } = require('../../utils') const Logger = require('../../Logger') class ServerSettings { @@ -144,7 +143,7 @@ class ServerSettings { this.authGoogleOauth20ClientSecret === '' || this.authGoogleOauth20CallbackURL === '' )) { - this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1); + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1) } // remove uninitialized methods @@ -158,7 +157,7 @@ class ServerSettings { this.authOpenIDClientSecret === '' || this.authOpenIDCallbackURL === '' )) { - this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('generic-oauth20', 0), 1); + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('generic-oauth20', 0), 1) } // fallback to local @@ -241,10 +240,10 @@ class ServerSettings { } update(payload) { - var hasUpdates = false + let hasUpdates = false for (const key in payload) { if (key === 'sortingPrefixes' && payload[key] && payload[key].length) { - var prefixesCleaned = payload[key].filter(prefix => !!prefix).map(prefix => prefix.toLowerCase()) + const prefixesCleaned = payload[key].filter(prefix => !!prefix).map(prefix => prefix.toLowerCase()) if (prefixesCleaned.join(',') !== this[key].join(',')) { this[key] = [...prefixesCleaned] hasUpdates = true From 4359ca28dfb034d27c1847382554102b1305632b Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 29 Apr 2023 16:05:05 -0500 Subject: [PATCH 005/208] Fix XAccel issue --- server/Server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Server.js b/server/Server.js index 96896a73e..1559a88e5 100644 --- a/server/Server.js +++ b/server/Server.js @@ -52,7 +52,7 @@ class Server { global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH)) global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH)) global.RouterBasePath = ROUTER_BASE_PATH - global.XAccel = process.env.USE_X_ACCELAuth + global.XAccel = process.env.USE_X_ACCEL if (!fs.pathExistsSync(global.ConfigPath)) { fs.mkdirSync(global.ConfigPath) From 405c954b65fa1cf76b4cd9374ec0cb62b7ff61f5 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 13 Sep 2023 16:35:39 +0000 Subject: [PATCH 006/208] Updated + first rough implementation --- client/pages/login.vue | 21 ++++++++++----- server/Auth.js | 54 +++++++++++++++++++++++++++------------ server/Server.js | 6 ++++- server/SocketAuthority.js | 29 ++++++++++++++++++++- server/models/User.js | 19 ++++++++++++++ 5 files changed, 104 insertions(+), 25 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 52feedb68..27bdc63b4 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -37,6 +37,15 @@ {{ processing ? 'Checking...' : $strings.ButtonSubmit }} +
+ @@ -132,11 +141,7 @@ export default { location.reload() }, -<<<<<<< HEAD - setUser({ user, userDefaultLibraryId, serverSettings, Source }) { -======= setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices }) { ->>>>>>> origin/master this.$store.commit('setServerSettings', serverSettings) this.$store.commit('setSource', Source) this.$store.commit('libraries/setEReaderDevices', ereaderDevices) @@ -166,10 +171,7 @@ export default { else this.error = 'Unknown Error' return false }) -<<<<<<< HEAD console.log('Auth res', authRes) -======= ->>>>>>> origin/master if (authRes?.error) { this.error = authRes.error } else if (authRes) { @@ -222,6 +224,11 @@ export default { } }, async mounted() { + console.log(new URLSearchParams(window.location.search).get('setToken')) + if (new URLSearchParams(window.location.search).get('setToken')) { + localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) + console.log('hereasd') + } if (localStorage.getItem('token')) { var userfound = await this.checkAuth() if (userfound) return // if valid user no need to check status diff --git a/server/Auth.js b/server/Auth.js index 0885c88a6..ceeddb366 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -6,14 +6,14 @@ const JwtStrategy = require('passport-jwt').Strategy const ExtractJwt = require('passport-jwt').ExtractJwt const GoogleStrategy = require('passport-google-oauth20').Strategy const OpenIDConnectStrategy = require('passport-openidconnect') +const Database = require('./Database') /** * @class Class for handling all the authentication related functionality. */ class Auth { - constructor(db) { - this.db = db + constructor() { } /** @@ -31,10 +31,10 @@ class Auth { clientID: global.ServerSettings.authGoogleOauth20ClientID, clientSecret: global.ServerSettings.authGoogleOauth20ClientSecret, callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL - }, (function (accessToken, refreshToken, profile, done) { + }, (async function (accessToken, refreshToken, profile, done) { // TODO: what to use as username // TODO: do we want to create the users which does not exist? - const user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) + const user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -61,7 +61,7 @@ class Auth { (function (issuer, profile, done) { // TODO: what to use as username // TODO: do we want to create the users which does not exist? - var user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) + var user = Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -86,16 +86,17 @@ class Auth { return cb(null, JSON.stringify({ "username": user.username, "id": user.id, + "email": user.email, })) }) }) // define how to deseralize a user (use the username to get it from the database) passport.deserializeUser((function (user, cb) { - process.nextTick((function () { + process.nextTick((async function () { const parsedUserInfo = JSON.parse(user) // TODO: do the matching on username or better on id? - const dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) + const dbUser = await Database.userModel.getUserByUsername(parsedUserInfo.username.toLowerCase()) return cb(null, dbUser) }).bind(this)) }).bind(this)) @@ -108,9 +109,9 @@ class Auth { initAuthRoutes(router) { // Local strategy login route (takes username and password) router.post('/login', passport.authenticate('local'), - (function (req, res) { + (async function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user)) + res.json(await this.getUserLoginResponsePayload(req.user)) }).bind(this) ) @@ -120,9 +121,12 @@ class Auth { // google-oauth20 strategy callback route (this receives the token from google) router.get('/auth/google/callback', passport.authenticate('google'), - (function (req, res) { + (async function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user)) + var data_json = await this.getUserLoginResponsePayload(req.user) + // res.json(data_json) + // TODO: figure out how to redirect back to the app page + res.redirect(301, `http://localhost:3000/login?setToken=${data_json.user.token}`) }).bind(this) ) @@ -132,9 +136,12 @@ class Auth { // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', passport.authenticate('openidconnect'), - (function (req, res) { + (async function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user)) + var data_json = await this.getUserLoginResponsePayload(req.user) + // res.json(data_json) + // TODO: figure out how to redirect back to the app page + res.redirect(301, `http://localhost:3000/login?setToken=${data_json.user.token}`) }).bind(this) ) @@ -176,6 +183,20 @@ class Auth { return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret) } + /** + * Function to generate a jwt token for a given user. + * @param {string} token + * @returns the tokens data. + */ + static validateAccessToken(token) { + try { + return jwt.verify(token, global.ServerSettings.tokenSecret) + } + catch (err) { + return null + } + } + /** * Generate a token for each user. */ @@ -203,7 +224,7 @@ class Auth { * @param {function} done */ jwtAuthCheck(jwt_payload, done) { - const user = this.db.users.find(u => u.username.toLowerCase() === jwt_payload.username.toLowerCase()) + const user = Database.userModel.getUserByUsername(jwt_payload.username.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -220,7 +241,7 @@ class Auth { * @param {function} done */ async localAuthCheckUserPw(username, password, done) { - const user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) + const user = Database.userModel.getUserByUsername(username.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -269,7 +290,8 @@ class Auth { * @param {string} username * @returns {string} jsonPayload */ - getUserLoginResponsePayload(user) { + async getUserLoginResponsePayload(user) { + const libraryIds = await Database.libraryModel.getAllLibraryIds() return { user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(libraryIds), diff --git a/server/Server.js b/server/Server.js index 9156c021b..899857689 100644 --- a/server/Server.js +++ b/server/Server.js @@ -161,7 +161,8 @@ class Server { this.server = http.createServer(app) - router.use(this.auth.cors) + + router.use(fileUpload({ defCharset: 'utf8', defParamCharset: 'utf8', @@ -195,6 +196,9 @@ class Server { this.rssFeedManager.getFeedItem(req, res) }) + // Auth routes + this.auth.initAuthRoutes(router) + // Client dynamic routes const dyanimicRoutes = [ '/item/:id', diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 5c3c8715e..81136ecfb 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -1,6 +1,9 @@ const SocketIO = require('socket.io') const Logger = require('./Logger') const Database = require('./Database') +const Auth = require('./Auth') +const passport = require('passport') +const expressSession = require('express-session') class SocketAuthority { constructor() { @@ -81,6 +84,24 @@ class SocketAuthority { methods: ["GET", "POST"] } }) + + /* + const wrap = middleware => (socket, next) => middleware(socket.request, {}, next); + + io.use(wrap(expressSession({ + secret: global.ServerSettings.tokenSecret, + resave: false, + saveUninitialized: false, + cookie: { + // also send the cookie if were hare not on https + secure: false + }, + }))); + + io.use(wrap(passport.initialize())); + io.use(wrap(passport.session())); + */ + this.io.on('connection', (socket) => { this.clients[socket.id] = { id: socket.id, @@ -147,7 +168,13 @@ class SocketAuthority { // When setting up a socket connection the user needs to be associated with a socket id // for this the client will send a 'auth' event that includes the users API token async authenticateSocket(socket, token) { - const user = await this.Server.db.users.find(u => u.token === token) + // TODO + const token_data = Auth.validateAccessToken(token) + if (!token_data || !token_data.username) { + Logger.error('Cannot validate socket - invalid token') + return socket.emit('invalid_token') + } + const user = await Database.userModel.getUserByUsername(token_data.username) if (!user) { Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') diff --git a/server/models/User.js b/server/models/User.js index 6f457aa5b..5e184fc75 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -194,6 +194,25 @@ class User extends Model { return this.getOldUser(user) } + /** + * Get user by email case insensitive + * @param {string} username + * @returns {Promise} returns null if not found + */ + static async getUserByEmail(email) { + if (!email) return null + const user = await this.findOne({ + where: { + email: { + [Op.like]: email + } + }, + include: this.sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + /** * Get user by id * @param {string} userId From af4c35069be11663deeed76fe26a73105032d87b Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Thu, 14 Sep 2023 18:49:19 +0100 Subject: [PATCH 007/208] Use a short-time cookie to remember where to callback to --- client/pages/login.vue | 7 +- package-lock.json | 2162 +++++++++++++++++++++++++++++++++++++++- package.json | 5 +- server/Auth.js | 49 +- server/Server.js | 3 + 5 files changed, 2212 insertions(+), 14 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 27bdc63b4..1b17ab50d 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -39,10 +39,10 @@
@@ -69,7 +69,8 @@ export default { }, confirmPassword: '', ConfigPath: '', - MetadataPath: '' + MetadataPath: '', + currentUrl: location.toString() } }, watch: { diff --git a/package-lock.json b/package-lock.json index 397babb5f..967439f24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", + "cookie-parser": "^1.4.6", "express": "^4.17.1", "express-session": "^1.17.3", "graceful-fs": "^4.2.10", @@ -533,6 +534,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -2881,5 +2902,2144 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } + }, + "dependencies": { + "@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "dependencies": { + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "requires": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "optional": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "requires": { + "@types/node": "*" + } + }, + "@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "requires": { + "@types/ms": "*" + } + }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, + "@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + }, + "@types/validator": { + "version": "13.11.1", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.1.tgz", + "integrity": "sha512-d/MUkJYdOeKycmm75Arql4M5+UuXmf4cHdHKsyw1GcvnNgL6s77UkgSgJ8TE/rI5PYsnwYq5jkcWBLuN/MpQ1A==" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "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==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "optional": true, + "requires": { + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "requires": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "requires": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + } + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" + }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" + } + }, + "dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "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==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "engine.io": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", + "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-parser": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", + "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==" + }, + "entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==" + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "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==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + } + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + } + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "htmlparser2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", + "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "entities": "^4.3.0" + } + }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "optional": true + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "optional": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "optional": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "optional": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "optional": true + }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } + } + }, + "make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "requires": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, + "moment-timezone": { + "version": "0.5.43", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz", + "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==", + "requires": { + "moment": "^2.29.4" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" + }, + "node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "requires": { + "abbrev": "1" + } + }, + "npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "node-tone": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz", + "integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w==" + }, + "nodemailer": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz", + "integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==" + }, + "nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "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==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "requires": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "passport-oauth2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", + "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", + "requires": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, + "passport-openidconnect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", + "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", + "requires": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true + }, + "retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "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==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "sequelize": { + "version": "6.32.1", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.32.1.tgz", + "integrity": "sha512-3Iv0jruv57Y0YvcxQW7BE56O7DC1BojcfIrqh6my+IQwde+9u/YnuYHzK+8kmZLhLvaziRT1eWu38nh9yVwn/g==", + "requires": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.4", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.0", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.1", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==" + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "optional": true + }, + "socket.io": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz", + "integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.2.1", + "socket.io-adapter": "~2.4.0", + "socket.io-parser": "~4.2.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-adapter": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", + "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==" + }, + "socket.io-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz", + "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "optional": true, + "requires": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "optional": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "sqlite3": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", + "integrity": "sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==", + "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", + "node-addon-api": "^4.2.0", + "node-gyp": "8.x", + "tar": "^6.1.11" + } + }, + "ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "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==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + } + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, + "uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "requires": { + "@types/node": "*" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "requires": {} + }, + "xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 182c30337..8c7656407 100644 --- a/package.json +++ b/package.json @@ -31,16 +31,17 @@ "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", + "cookie-parser": "^1.4.6", "express": "^4.17.1", "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "node-tone": "^1.0.1", + "nodemailer": "^6.9.2", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "passport-openidconnect": "^0.1.1", - "nodemailer": "^6.9.2", "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", @@ -49,4 +50,4 @@ "devDependencies": { "nodemon": "^2.0.20" } -} \ No newline at end of file +} diff --git a/server/Auth.js b/server/Auth.js index ceeddb366..a219129be 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -32,7 +32,6 @@ class Auth { clientSecret: global.ServerSettings.authGoogleOauth20ClientSecret, callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL }, (async function (accessToken, refreshToken, profile, done) { - // TODO: what to use as username // TODO: do we want to create the users which does not exist? const user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) @@ -59,7 +58,6 @@ class Auth { skipUserProfile: false }, (function (issuer, profile, done) { - // TODO: what to use as username // TODO: do we want to create the users which does not exist? var user = Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) @@ -116,7 +114,20 @@ class Auth { ) // google-oauth20 strategy login route (this redirects to the google login) - router.get('/auth/google', passport.authenticate('google', { scope: ['email'] })) + router.get('/auth/google', (req, res, next) => { + const auth_func = passport.authenticate('google', { scope: ['email'] }) + if (!req.query.callback || req.query.callback === "") { + res.status(400).send({ + message: 'No callback parameter' + }) + return + } + res.cookie('auth_cb', req.query.callback, { + maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + httpOnly: true + }) + auth_func(req, res, next); + }) // google-oauth20 strategy callback route (this receives the token from google) router.get('/auth/google/callback', @@ -125,13 +136,31 @@ class Auth { // return the user login response json if the login was successfull var data_json = await this.getUserLoginResponsePayload(req.user) // res.json(data_json) - // TODO: figure out how to redirect back to the app page - res.redirect(301, `http://localhost:3000/login?setToken=${data_json.user.token}`) + // TODO: do we want to somehow limit the values for auth_cb? + if (req.cookies.auth_cb) { + res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) + } + else { + res.status(400).send("No callback or already expired") + } }).bind(this) ) // openid strategy login route (this redirects to the configured openid login provider) - router.get('/auth/openid', passport.authenticate('openidconnect')) + router.get('/auth/openid', (req, res, next) => { + const auth_func = passport.authenticate('openidconnect') + if (!req.query.callback || req.query.callback === "") { + res.status(400).send({ + message: 'No callback parameter' + }) + return + } + res.cookie('auth_cb', req.query.callback, { + maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + httpOnly: true + }) + auth_func(req, res, next); + }) // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', @@ -140,8 +169,12 @@ class Auth { // return the user login response json if the login was successfull var data_json = await this.getUserLoginResponsePayload(req.user) // res.json(data_json) - // TODO: figure out how to redirect back to the app page - res.redirect(301, `http://localhost:3000/login?setToken=${data_json.user.token}`) + if (req.cookies.auth_cb) { + res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) + } + else { + res.status(400).send("No callback or already expired") + } }).bind(this) ) diff --git a/server/Server.js b/server/Server.js index 899857689..ad7fa16cd 100644 --- a/server/Server.js +++ b/server/Server.js @@ -5,6 +5,7 @@ const http = require('http') const fs = require('./libs/fsExtra') const fileUpload = require('./libs/expressFileupload') const rateLimit = require('./libs/expressRateLimit') +const cookieParser = require("cookie-parser"); const { version } = require('../package.json') @@ -136,6 +137,8 @@ class Server { const app = express() + // parse cookies in requests + app.use(cookieParser()); // enable express-session app.use(expressSession({ secret: global.ServerSettings.tokenSecret, From 6aaf3f0f025f2c95caa77055cedd77c1bb995615 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 16 Sep 2023 18:22:11 +0000 Subject: [PATCH 008/208] Fix bug with undefined property --- server/Auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index a219129be..dc21aa378 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -235,9 +235,9 @@ class Auth { */ async initTokenSecret() { if (process.env.TOKEN_SECRET) { // User can supply their own token secret - this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET + global.ServerSettings.tokenSecret = process.env.TOKEN_SECRET } else { - this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') + global.ServerSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') } await Database.updateServerSettings() From 91d8451ab38d45c10709ed7ea52d461e00ecd753 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 16 Sep 2023 18:22:23 +0000 Subject: [PATCH 009/208] Remove log messages --- client/pages/login.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 1b17ab50d..beac44ff0 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -172,7 +172,7 @@ export default { else this.error = 'Unknown Error' return false }) - console.log('Auth res', authRes) + if (authRes?.error) { this.error = authRes.error } else if (authRes) { @@ -225,10 +225,8 @@ export default { } }, async mounted() { - console.log(new URLSearchParams(window.location.search).get('setToken')) if (new URLSearchParams(window.location.search).get('setToken')) { localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) - console.log('hereasd') } if (localStorage.getItem('token')) { var userfound = await this.checkAuth() From 7af3033f8d46275f105e4701455caa4b710013a5 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 16 Sep 2023 18:42:48 +0000 Subject: [PATCH 010/208] Fix: ci error - no token sercret --- server/Auth.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/Auth.js b/server/Auth.js index dc21aa378..c8376ff61 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -19,7 +19,7 @@ class Auth { /** * Inializes all passportjs stragegies and other passportjs ralated initialization. */ - initPassportJs() { + async initPassportJs() { // Check if we should load the local strategy if (global.ServerSettings.authActiveAuthMethods.includes("local")) { passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) @@ -70,6 +70,10 @@ class Auth { }).bind(this))) } + if (!global.ServerSettings.tokenSecret) { + await this.initTokenSecret() + } + // Load the JwtStrategy (always) -> for bearer token auth passport.use(new JwtStrategy({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), From 763c0f4a3df1bd4dce5c037aec0ae3d61e84633d Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 16 Sep 2023 18:51:29 +0000 Subject: [PATCH 011/208] add missing await --- server/Server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Server.js b/server/Server.js index f306a61d4..7cc6fa769 100644 --- a/server/Server.js +++ b/server/Server.js @@ -154,7 +154,7 @@ class Server { // register passport in express-session app.use(passport.session()) // config passport.js - this.auth.initPassportJs() + await this.auth.initPassportJs() // use auth on all routes - not used now // app.use(passport.authenticate('session')) From 942aa93f574af4a6b5ac8724c548b1ac484ef6cb Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 16 Sep 2023 19:45:04 +0000 Subject: [PATCH 012/208] Fix: local login not possible --- server/Auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Auth.js b/server/Auth.js index c8376ff61..0e62d7dd6 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -278,7 +278,7 @@ class Auth { * @param {function} done */ async localAuthCheckUserPw(username, password, done) { - const user = Database.userModel.getUserByUsername(username.toLowerCase()) + const user = await Database.userModel.getUserByUsername(username.toLowerCase()) if (!user || !user.isActive) { done(null, null) From 0a6cd8909068646db8c09d2b737b9e5f4eda240e Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sun, 17 Sep 2023 18:42:42 +0100 Subject: [PATCH 013/208] Allow rest mode login (?isRest=true) --- server/Auth.js | 108 ++++++++++++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 45 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 0e62d7dd6..17fea2479 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -104,6 +104,63 @@ class Auth { }).bind(this)) } + /** + * Stores the client's choise how the login callback should happen in temp cookies. + * @param {*} req Request object. + * @param {*} res Response object. + */ + paramsToCookies(req, res) { + if (req.query.isRest && (req.query.isRest.toLowerCase() == "true" || req.query.isRest.toLowerCase() == "false")) { + res.cookie('is_rest', req.query.isRest.toLowerCase(), { + maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + httpOnly: true + }) + } + else { + res.cookie('is_rest', "false", { + maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + httpOnly: true + }) + if (!req.query.callback || req.query.callback === "") { + res.status(400).send({ + message: 'No callback parameter' + }) + return + } + res.cookie('auth_cb', req.query.callback, { + maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + httpOnly: true + }) + } + } + + + /** + * Informs the client in the right mode about a successfull login and the token + * (clients choise is restored from cookies). + * @param {*} req Request object. + * @param {*} res Response object. + */ + async handleLoginSuccessBasedOnCookie(req, res) { + const data_json = await this.getUserLoginResponsePayload(req.user) + + if (req.cookies.is_rest && req.cookies.is_rest === "true") { + // REST request - send data + res.json(data_json) + } + else { + // UI request -> check if we have a callback url + // TODO: do we want to somehow limit the values for auth_cb? + if (req.cookies.auth_cb && req.cookies.auth_cb.startsWith("http")) { + // UI request -> redirect + res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) + } + else { + res.status(400).send("No callback or already expired") + } + } + } + /** * Creates all (express) routes required for authentication. * @param {express.Router} router @@ -120,66 +177,27 @@ class Auth { // google-oauth20 strategy login route (this redirects to the google login) router.get('/auth/google', (req, res, next) => { const auth_func = passport.authenticate('google', { scope: ['email'] }) - if (!req.query.callback || req.query.callback === "") { - res.status(400).send({ - message: 'No callback parameter' - }) - return - } - res.cookie('auth_cb', req.query.callback, { - maxAge: 120000 * 120, // Hack - this semms to be in UTC?? - httpOnly: true - }) - auth_func(req, res, next); + this.paramsToCookies(req, res) + auth_func(req, res, next) }) // google-oauth20 strategy callback route (this receives the token from google) router.get('/auth/google/callback', passport.authenticate('google'), - (async function (req, res) { - // return the user login response json if the login was successfull - var data_json = await this.getUserLoginResponsePayload(req.user) - // res.json(data_json) - // TODO: do we want to somehow limit the values for auth_cb? - if (req.cookies.auth_cb) { - res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) - } - else { - res.status(400).send("No callback or already expired") - } - }).bind(this) + this.handleLoginSuccessBasedOnCookie.bind(this) ) // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { const auth_func = passport.authenticate('openidconnect') - if (!req.query.callback || req.query.callback === "") { - res.status(400).send({ - message: 'No callback parameter' - }) - return - } - res.cookie('auth_cb', req.query.callback, { - maxAge: 120000 * 120, // Hack - this semms to be in UTC?? - httpOnly: true - }) - auth_func(req, res, next); + this.paramsToCookies(req, res) + auth_func(req, res, next) }) // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', passport.authenticate('openidconnect'), - (async function (req, res) { - // return the user login response json if the login was successfull - var data_json = await this.getUserLoginResponsePayload(req.user) - // res.json(data_json) - if (req.cookies.auth_cb) { - res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) - } - else { - res.status(400).send("No callback or already expired") - } - }).bind(this) + this.handleLoginSuccessBasedOnCookie.bind(this) ) // Logout route From 2c90bba774ac107e4bdaa38248803c90083cde3c Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 20 Sep 2023 18:37:55 +0100 Subject: [PATCH 014/208] small refactorings --- server/Auth.js | 57 ++++++++++++++++++++++++++++----------- server/Server.js | 7 +++-- server/SocketAuthority.js | 29 +++++--------------- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 17fea2479..d68ef8765 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -17,10 +17,10 @@ class Auth { } /** - * Inializes all passportjs stragegies and other passportjs ralated initialization. + * Inializes all passportjs strategies and other passportjs ralated initialization. */ async initPassportJs() { - // Check if we should load the local strategy + // Check if we should load the local strategy (username + password login) if (global.ServerSettings.authActiveAuthMethods.includes("local")) { passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) } @@ -33,13 +33,17 @@ class Auth { callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL }, (async function (accessToken, refreshToken, profile, done) { // TODO: do we want to create the users which does not exist? + + // get user by email const user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { + // deny login done(null, null) return } + // permit login return done(null, user) }).bind(this))) } @@ -59,17 +63,23 @@ class Auth { }, (function (issuer, profile, done) { // TODO: do we want to create the users which does not exist? + + // get user by email var user = Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { + // deny login done(null, null) return } + // permit login return done(null, user) }).bind(this))) } + // should be already initialied here - but ci had some problems so check again + // token is required to encrypt/protect the info in jwts if (!global.ServerSettings.tokenSecret) { await this.initTokenSecret() } @@ -83,22 +93,19 @@ class Auth { // define how to seralize a user (to be put into the session) passport.serializeUser(function (user, cb) { process.nextTick(function () { - // only store username and id to session - // TODO: do we want to store more info in the session? + // only store id to session return cb(null, JSON.stringify({ - "username": user.username, "id": user.id, - "email": user.email, })) }) }) - // define how to deseralize a user (use the username to get it from the database) + // define how to deseralize a user (use the ID to get it from the database) passport.deserializeUser((function (user, cb) { process.nextTick((async function () { const parsedUserInfo = JSON.parse(user) - // TODO: do the matching on username or better on id? - const dbUser = await Database.userModel.getUserByUsername(parsedUserInfo.username.toLowerCase()) + // load the user by ID that is stored in the session + const dbUser = await Database.userModel.getUserById(parsedUserInfo.id) return cb(null, dbUser) }).bind(this)) }).bind(this)) @@ -110,23 +117,28 @@ class Auth { * @param {*} res Response object. */ paramsToCookies(req, res) { - if (req.query.isRest && (req.query.isRest.toLowerCase() == "true" || req.query.isRest.toLowerCase() == "false")) { + if (req.query.isRest && req.query.isRest.toLowerCase() == "true") { + // store the isRest flag to the is_rest cookie res.cookie('is_rest', req.query.isRest.toLowerCase(), { maxAge: 120000 * 120, // Hack - this semms to be in UTC?? httpOnly: true }) } else { + // no isRest-flag set -> set is_rest cookie to false res.cookie('is_rest', "false", { maxAge: 120000 * 120, // Hack - this semms to be in UTC?? httpOnly: true }) + + // check if we are missing a callback parameter - we need one if isRest=false if (!req.query.callback || req.query.callback === "") { res.status(400).send({ message: 'No callback parameter' }) return } + // store the callback url to the auth_cb cookie res.cookie('auth_cb', req.query.callback, { maxAge: 120000 * 120, // Hack - this semms to be in UTC?? httpOnly: true @@ -142,6 +154,7 @@ class Auth { * @param {*} res Response object. */ async handleLoginSuccessBasedOnCookie(req, res) { + // get userLogin json (information about the user, server and the session) const data_json = await this.getUserLoginResponsePayload(req.user) if (req.cookies.is_rest && req.cookies.is_rest === "true") { @@ -152,7 +165,7 @@ class Auth { // UI request -> check if we have a callback url // TODO: do we want to somehow limit the values for auth_cb? if (req.cookies.auth_cb && req.cookies.auth_cb.startsWith("http")) { - // UI request -> redirect + // UI request -> redirect to auth_cb url and send the jwt token as parameter res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) } else { @@ -165,7 +178,7 @@ class Auth { * Creates all (express) routes required for authentication. * @param {express.Router} router */ - initAuthRoutes(router) { + async initAuthRoutes(router) { // Local strategy login route (takes username and password) router.post('/login', passport.authenticate('local'), (async function (req, res) { @@ -177,6 +190,7 @@ class Auth { // google-oauth20 strategy login route (this redirects to the google login) router.get('/auth/google', (req, res, next) => { const auth_func = passport.authenticate('google', { scope: ['email'] }) + // params (isRest, callback) to a cookie that will be send to the client this.paramsToCookies(req, res) auth_func(req, res, next) }) @@ -184,12 +198,14 @@ class Auth { // google-oauth20 strategy callback route (this receives the token from google) router.get('/auth/google/callback', passport.authenticate('google'), + // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this) ) // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { const auth_func = passport.authenticate('openidconnect') + // params (isRest, callback) to a cookie that will be send to the client this.paramsToCookies(req, res) auth_func(req, res, next) }) @@ -197,6 +213,7 @@ class Auth { // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', passport.authenticate('openidconnect'), + // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this) ) @@ -239,7 +256,7 @@ class Auth { } /** - * Function to generate a jwt token for a given user. + * Function to validate a jwt token for a given user. * @param {string} token * @returns the tokens data. */ @@ -253,7 +270,7 @@ class Auth { } /** - * Generate a token for each user. + * Generate a token which is used to encrpt/protect the jwts. */ async initTokenSecret() { if (process.env.TOKEN_SECRET) { // User can supply their own token secret @@ -279,12 +296,15 @@ class Auth { * @param {function} done */ jwtAuthCheck(jwt_payload, done) { - const user = Database.userModel.getUserByUsername(jwt_payload.username.toLowerCase()) + // load user by id from the jwt token + const user = Database.userModel.getUserById(jwt_payload.id) if (!user || !user.isActive) { + // deny login done(null, null) return } + // approve login done(null, user) return } @@ -296,6 +316,7 @@ class Auth { * @param {function} done */ async localAuthCheckUserPw(username, password, done) { + // Load the user given it's username const user = await Database.userModel.getUserByUsername(username.toLowerCase()) if (!user || !user.isActive) { @@ -306,9 +327,11 @@ class Auth { // Check passwordless root user if (user.id === 'root' && (!user.pash || user.pash === '')) { if (password) { + // deny login done(null, null) return } + // approve login done(null, user) return } @@ -316,9 +339,11 @@ class Auth { // Check password match const compare = await bcrypt.compare(password, user.pash) if (compare) { + // approve login done(null, user) return } + // deny login done(null, null) return } @@ -343,7 +368,7 @@ class Auth { /** * Return the login info payload for a user. * @param {string} username - * @returns {string} jsonPayload + * @returns {Promise} jsonPayload */ async getUserLoginResponsePayload(user) { const libraryIds = await Database.libraryModel.getAllLibraryIds() diff --git a/server/Server.js b/server/Server.js index 7cc6fa769..cf55061d8 100644 --- a/server/Server.js +++ b/server/Server.js @@ -87,6 +87,7 @@ class Server { } authMiddleware(req, res, next) { + // ask passportjs if the current request is authenticated this.auth.isAuthenticated(req, res, next) } @@ -145,7 +146,7 @@ class Server { resave: false, saveUninitialized: false, cookie: { - // also send the cookie if were hare not on https + // also send the cookie if were are not on https (not every use has https) secure: false }, })) @@ -155,8 +156,6 @@ class Server { app.use(passport.session()) // config passport.js await this.auth.initPassportJs() - // use auth on all routes - not used now - // app.use(passport.authenticate('session')) const router = express.Router() app.use(global.RouterBasePath, router) @@ -200,7 +199,7 @@ class Server { }) // Auth routes - this.auth.initAuthRoutes(router) + await this.auth.initAuthRoutes(router) // Client dynamic routes const dyanimicRoutes = [ diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 81136ecfb..28e59e40e 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -2,8 +2,6 @@ const SocketIO = require('socket.io') const Logger = require('./Logger') const Database = require('./Database') const Auth = require('./Auth') -const passport = require('passport') -const expressSession = require('express-session') class SocketAuthority { constructor() { @@ -85,23 +83,6 @@ class SocketAuthority { } }) - /* - const wrap = middleware => (socket, next) => middleware(socket.request, {}, next); - - io.use(wrap(expressSession({ - secret: global.ServerSettings.tokenSecret, - resave: false, - saveUninitialized: false, - cookie: { - // also send the cookie if were hare not on https - secure: false - }, - }))); - - io.use(wrap(passport.initialize())); - io.use(wrap(passport.session())); - */ - this.io.on('connection', (socket) => { this.clients[socket.id] = { id: socket.id, @@ -168,14 +149,18 @@ class SocketAuthority { // When setting up a socket connection the user needs to be associated with a socket id // for this the client will send a 'auth' event that includes the users API token async authenticateSocket(socket, token) { - // TODO + // we don't use passport to authenticate the jwt we get over the socket connection. + // it's easier to directly verify/decode it. const token_data = Auth.validateAccessToken(token) - if (!token_data || !token_data.username) { + if (!token_data || !token_data.id) { + // Token invalid Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } - const user = await Database.userModel.getUserByUsername(token_data.username) + // get the user via the id from the decoded jwt. + const user = await Database.userModel.getUserById(token_data.id) if (!user) { + // user not found Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } From f6113e85c774314dd605e0500251c14c7834353f Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 20 Sep 2023 18:48:57 +0100 Subject: [PATCH 015/208] cookie lifetime --- server/Auth.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index d68ef8765..332e500d1 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -120,14 +120,14 @@ class Auth { if (req.query.isRest && req.query.isRest.toLowerCase() == "true") { // store the isRest flag to the is_rest cookie res.cookie('is_rest', req.query.isRest.toLowerCase(), { - maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + maxAge: 120000, // 2 min httpOnly: true }) } else { // no isRest-flag set -> set is_rest cookie to false res.cookie('is_rest', "false", { - maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + maxAge: 120000, // 2 min httpOnly: true }) @@ -140,7 +140,7 @@ class Auth { } // store the callback url to the auth_cb cookie res.cookie('auth_cb', req.query.callback, { - maxAge: 120000 * 120, // Hack - this semms to be in UTC?? + maxAge: 120000, // 2 min httpOnly: true }) } From 45cf00bd0498e7c10b39921a2133e67aae7a8b4e Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 20 Sep 2023 19:06:16 +0100 Subject: [PATCH 016/208] fix openid + jwt auth --- server/Auth.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 332e500d1..f80747768 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -61,11 +61,11 @@ class Auth { scope: ["openid", "email", "profile"], skipUserProfile: false }, - (function (issuer, profile, done) { + (async function (issuer, profile, done) { // TODO: do we want to create the users which does not exist? // get user by email - var user = Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) + var user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { // deny login @@ -295,9 +295,9 @@ class Auth { * @param {Object} jwt_payload * @param {function} done */ - jwtAuthCheck(jwt_payload, done) { + async jwtAuthCheck(jwt_payload, done) { // load user by id from the jwt token - const user = Database.userModel.getUserById(jwt_payload.id) + const user = await Database.userModel.getUserById(jwt_payload.id) if (!user || !user.isActive) { // deny login From 2c25f64652366cb1b542e8a54f8dc5f096588357 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 20 Sep 2023 19:16:08 +0100 Subject: [PATCH 017/208] Add /auth_methods route --- server/Auth.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/Auth.js b/server/Auth.js index f80747768..15be664cb 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -228,6 +228,11 @@ class Auth { } }) }) + + // Get avilible auth methods + router.get('/auth_methods', (req, res) => { + res.json(global.ServerSettings.authActiveAuthMethods) + }) } /** From 0e75c8062725eab5d0445eec4d0a3c22bfa46bc5 Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Wed, 20 Sep 2023 19:45:32 +0100 Subject: [PATCH 018/208] prepare show/hide of login buttons --- client/pages/login.vue | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index beac44ff0..29341ed39 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -27,7 +27,7 @@

{{ $strings.HeaderLogin }}

{{ error }}

-
+ @@ -39,10 +39,10 @@
@@ -222,9 +222,28 @@ export default { this.processing = false this.criticalError = 'Status check failed' }) + }, + async updateLoginVisibility() { + await this.$axios + .$get('/auth_methods') + .then((response) => { + ;['local', 'google-oauth20', 'openid'].forEach((auth_method) => { + debugger + if (response.includes(auth_method)) { + // TODO: show `#login-${auth_method}` + } else { + // TODO: hide `#login-${auth_method}` + } + }) + }) + .catch((error) => { + console.error('Failed', error.response) + return false + }) } }, async mounted() { + this.updateLoginVisibility() if (new URLSearchParams(window.location.search).get('setToken')) { localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) } From 7a131880e565da35c6bd774ca6d4b88a4092f5da Mon Sep 17 00:00:00 2001 From: lukeIam <2lukeiam@gmail.com> Date: Sat, 23 Sep 2023 17:00:14 +0100 Subject: [PATCH 019/208] show/hide of login buttons --- client/pages/login.vue | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 29341ed39..f9d820873 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -27,7 +27,7 @@

{{ $strings.HeaderLogin }}

{{ error }}

-
+ @@ -39,10 +39,10 @@
@@ -70,7 +70,10 @@ export default { confirmPassword: '', ConfigPath: '', MetadataPath: '', - currentUrl: location.toString() + currentUrl: location.toString(), + login_local: true, + login_google_oauth20: false, + login_openid: false } }, watch: { @@ -227,14 +230,23 @@ export default { await this.$axios .$get('/auth_methods') .then((response) => { - ;['local', 'google-oauth20', 'openid'].forEach((auth_method) => { - debugger - if (response.includes(auth_method)) { - // TODO: show `#login-${auth_method}` - } else { - // TODO: hide `#login-${auth_method}` - } - }) + if (response.includes('local')) { + this.login_local = true + } else { + this.login_local = false + } + + if (response.includes('google-oauth20')) { + this.login_google_oauth20 = true + } else { + this.login_google_oauth20 = false + } + + if (response.includes('openid')) { + this.login_openid = true + } else { + this.login_openid = false + } }) .catch((error) => { console.error('Failed', error.response) @@ -243,7 +255,7 @@ export default { } }, async mounted() { - this.updateLoginVisibility() + this.$nextTick(async () => await this.updateLoginVisibility()) if (new URLSearchParams(window.location.search).get('setToken')) { localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) } From f42ab45e1b81dd3e95536b4996c147af8b31b8f4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 23 Sep 2023 13:30:28 -0500 Subject: [PATCH 020/208] Update passwordless root user check to user user.type instead of user.id --- server/Auth.js | 10 ++-------- server/Server.js | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 15be664cb..05044f74c 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -78,16 +78,10 @@ class Auth { }).bind(this))) } - // should be already initialied here - but ci had some problems so check again - // token is required to encrypt/protect the info in jwts - if (!global.ServerSettings.tokenSecret) { - await this.initTokenSecret() - } - // Load the JwtStrategy (always) -> for bearer token auth passport.use(new JwtStrategy({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: global.ServerSettings.tokenSecret + secretOrKey: Database.serverSettings.tokenSecret }, this.jwtAuthCheck.bind(this))) // define how to seralize a user (to be put into the session) @@ -330,7 +324,7 @@ class Auth { } // Check passwordless root user - if (user.id === 'root' && (!user.pash || user.pash === '')) { + if (user.type === 'root' && (!user.pash || user.pash === '')) { if (password) { // deny login done(null, null) diff --git a/server/Server.js b/server/Server.js index cf55061d8..2424456d9 100644 --- a/server/Server.js +++ b/server/Server.js @@ -139,7 +139,7 @@ class Server { const app = express() // parse cookies in requests - app.use(cookieParser()); + app.use(cookieParser()) // enable express-session app.use(expressSession({ secret: global.ServerSettings.tokenSecret, From 9922294507f6152f15f801ff713492d16c5ff256 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 23 Sep 2023 13:42:28 -0500 Subject: [PATCH 021/208] Fix setting tokenSecret on init --- server/Auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 05044f74c..13d7cde93 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -273,9 +273,9 @@ class Auth { */ async initTokenSecret() { if (process.env.TOKEN_SECRET) { // User can supply their own token secret - global.ServerSettings.tokenSecret = process.env.TOKEN_SECRET + Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET } else { - global.ServerSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') + Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') } await Database.updateServerSettings() From f6de373388f2065e8c15c0d279fcaaae2bee1fce Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 24 Sep 2023 12:36:36 -0500 Subject: [PATCH 022/208] Update /status endpoint to return available auth methods, fix socket auth, update openid to use username instead of email --- client/pages/login.vue | 69 ++++++++++++++++++++------------------- server/Auth.js | 59 ++++++++++++++++----------------- server/Server.js | 3 +- server/SocketAuthority.js | 15 ++++++--- 4 files changed, 76 insertions(+), 70 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index f9d820873..73cd27679 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -25,8 +25,11 @@

{{ $strings.HeaderLogin }}

+
+

{{ error }}

+
@@ -37,7 +40,9 @@ {{ processing ? 'Checking...' : $strings.ButtonSubmit }}
-
+ +
+
Login with Google @@ -106,6 +111,9 @@ export default { computed: { user() { return this.$store.state.user.user + }, + googleAuthUri() { + return `${process.env.serverUrl}/auth/openid?callback=${currentUrl}` } }, methods: { @@ -210,14 +218,16 @@ export default { this.processing = true this.$axios .$get('/status') - .then((res) => { + .then((data) => { this.processing = false - this.isInit = res.isInit - this.showInitScreen = !res.isInit - this.$setServerLanguageCode(res.language) + this.isInit = data.isInit + this.showInitScreen = !data.isInit + this.$setServerLanguageCode(data.language) if (this.showInitScreen) { - this.ConfigPath = res.ConfigPath || '' - this.MetadataPath = res.MetadataPath || '' + this.ConfigPath = data.ConfigPath || '' + this.MetadataPath = data.MetadataPath || '' + } else { + this.updateLoginVisibility(data.authMethods || []) } }) .catch((error) => { @@ -226,43 +236,34 @@ export default { this.criticalError = 'Status check failed' }) }, - async updateLoginVisibility() { - await this.$axios - .$get('/auth_methods') - .then((response) => { - if (response.includes('local')) { - this.login_local = true - } else { - this.login_local = false - } + updateLoginVisibility(authMethods) { + if (authMethods.includes('local') || !authMethods.length) { + this.login_local = true + } else { + this.login_local = false + } - if (response.includes('google-oauth20')) { - this.login_google_oauth20 = true - } else { - this.login_google_oauth20 = false - } + if (authMethods.includes('google-oauth20')) { + this.login_google_oauth20 = true + } else { + this.login_google_oauth20 = false + } - if (response.includes('openid')) { - this.login_openid = true - } else { - this.login_openid = false - } - }) - .catch((error) => { - console.error('Failed', error.response) - return false - }) + if (authMethods.includes('openid')) { + this.login_openid = true + } else { + this.login_openid = false + } } }, async mounted() { - this.$nextTick(async () => await this.updateLoginVisibility()) if (new URLSearchParams(window.location.search).get('setToken')) { localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) } if (localStorage.getItem('token')) { - var userfound = await this.checkAuth() - if (userfound) return // if valid user no need to check status + if (await this.checkAuth()) return // if valid user no need to check status } + this.checkStatus() } } diff --git a/server/Auth.js b/server/Auth.js index 13d7cde93..0041fbed0 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -64,10 +64,9 @@ class Auth { (async function (issuer, profile, done) { // TODO: do we want to create the users which does not exist? - // get user by email - var user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) + const user = await Database.userModel.getUserByUsername(profile.username) - if (!user || !user.isActive) { + if (!user?.isActive) { // deny login done(null, null) return @@ -106,9 +105,10 @@ class Auth { } /** - * Stores the client's choise how the login callback should happen in temp cookies. - * @param {*} req Request object. - * @param {*} res Response object. + * Stores the client's choice how the login callback should happen in temp cookies + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ paramsToCookies(req, res) { if (req.query.isRest && req.query.isRest.toLowerCase() == "true") { @@ -140,12 +140,12 @@ class Auth { } } - /** * Informs the client in the right mode about a successfull login and the token * (clients choise is restored from cookies). - * @param {*} req Request object. - * @param {*} res Response object. + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async handleLoginSuccessBasedOnCookie(req, res) { // get userLogin json (information about the user, server and the session) @@ -170,16 +170,15 @@ class Auth { /** * Creates all (express) routes required for authentication. - * @param {express.Router} router + * + * @param {import('express').Router} router */ async initAuthRoutes(router) { // Local strategy login route (takes username and password) - router.post('/login', passport.authenticate('local'), - (async function (req, res) { - // return the user login response json if the login was successfull - res.json(await this.getUserLoginResponsePayload(req.user)) - }).bind(this) - ) + router.post('/login', passport.authenticate('local'), async (req, res) => { + // return the user login response json if the login was successfull + res.json(await this.getUserLoginResponsePayload(req.user)) + }) // google-oauth20 strategy login route (this redirects to the google login) router.get('/auth/google', (req, res, next) => { @@ -222,18 +221,13 @@ class Auth { } }) }) - - // Get avilible auth methods - router.get('/auth_methods', (req, res) => { - res.json(global.ServerSettings.authActiveAuthMethods) - }) } /** * middleware to use in express to only allow authenticated users. - * @param {express.Request} req - * @param {express.Response} res - * @param {express.NextFunction} next + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next */ isAuthenticated(req, res, next) { // check if session cookie says that we are authenticated @@ -246,18 +240,20 @@ class Auth { } /** - * Function to generate a jwt token for a given user. + * Function to generate a jwt token for a given user + * * @param {Object} user - * @returns the token. + * @returns {string} token */ generateAccessToken(user) { return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret) } /** - * Function to validate a jwt token for a given user. + * Function to validate a jwt token for a given user + * * @param {string} token - * @returns the tokens data. + * @returns {Object} tokens data */ static validateAccessToken(token) { try { @@ -365,9 +361,10 @@ class Auth { } /** - * Return the login info payload for a user. - * @param {string} username - * @returns {Promise} jsonPayload + * Return the login info payload for a user + * + * @param {Object} user + * @returns {Promise} jsonPayload */ async getUserLoginResponsePayload(user) { const libraryIds = await Database.libraryModel.getAllLibraryIds() diff --git a/server/Server.js b/server/Server.js index 2424456d9..dbb9bddf3 100644 --- a/server/Server.js +++ b/server/Server.js @@ -238,7 +238,8 @@ class Server { // server has been initialized if a root user exists const payload = { isInit: Database.hasRootUser, - language: Database.serverSettings.language + language: Database.serverSettings.language, + authMethods: Database.serverSettings.authActiveAuthMethods } if (!payload.isInit) { payload.ConfigPath = global.ConfigPath diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 28e59e40e..f1c69b24d 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -146,24 +146,31 @@ class SocketAuthority { }) } - // When setting up a socket connection the user needs to be associated with a socket id - // for this the client will send a 'auth' event that includes the users API token + /** + * When setting up a socket connection the user needs to be associated with a socket id + * for this the client will send a 'auth' event that includes the users API token + * + * @param {SocketIO.Socket} socket + * @param {string} token JWT + */ async authenticateSocket(socket, token) { // we don't use passport to authenticate the jwt we get over the socket connection. // it's easier to directly verify/decode it. const token_data = Auth.validateAccessToken(token) - if (!token_data || !token_data.id) { + + if (!token_data?.userId) { // Token invalid Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } // get the user via the id from the decoded jwt. - const user = await Database.userModel.getUserById(token_data.id) + const user = await Database.userModel.getUserByIdOrOldId(token_data.userId) if (!user) { // user not found Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } + const client = this.clients[socket.id] if (!client) { Logger.error(`[SocketAuthority] Socket for user ${user.username} has no client`) From 7ba10db7d4f10788c515e139cc732fbb5402eb24 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 24 Sep 2023 12:39:38 -0500 Subject: [PATCH 023/208] Update login button openid and google urls --- client/pages/login.vue | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 73cd27679..4ca7c302f 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -44,10 +44,10 @@
@@ -75,7 +75,6 @@ export default { confirmPassword: '', ConfigPath: '', MetadataPath: '', - currentUrl: location.toString(), login_local: true, login_google_oauth20: false, login_openid: false @@ -113,7 +112,10 @@ export default { return this.$store.state.user.user }, googleAuthUri() { - return `${process.env.serverUrl}/auth/openid?callback=${currentUrl}` + return `${process.env.serverUrl}/auth/google?callback=${location.toString()}` + }, + openidAuthUri() { + return `${process.env.serverUrl}/auth/openid?callback=${location.toString()}` } }, methods: { From e282142d3fcf3d483cbea8432c85e216fed6ca72 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 24 Sep 2023 15:36:35 -0500 Subject: [PATCH 024/208] Add authentication page in config, add /auth-settings GET endpoint, remove authOpenIDCallbackURL server setting --- client/components/app/ConfigSideNav.vue | 5 + client/pages/config.vue | 1 + client/pages/config/authentication.vue | 138 ++++++++++++++++++++++ client/store/index.js | 2 +- client/strings/en-us.json | 1 + server/Auth.js | 46 ++++---- server/Server.js | 2 - server/controllers/MiscController.js | 23 +++- server/objects/settings/ServerSettings.js | 51 ++++++-- server/routers/ApiRouter.js | 1 + 10 files changed, 225 insertions(+), 45 deletions(-) create mode 100644 client/pages/config/authentication.vue diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index 50e440d7b..677aba703 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -104,6 +104,11 @@ export default { id: 'config-rss-feeds', title: this.$strings.HeaderRSSFeeds, path: '/config/rss-feeds' + }, + { + id: 'config-authentication', + title: this.$strings.HeaderAuthentication, + path: '/config/authentication' } ] diff --git a/client/pages/config.vue b/client/pages/config.vue index 542b7f2cb..fdbd71508 100644 --- a/client/pages/config.vue +++ b/client/pages/config.vue @@ -57,6 +57,7 @@ export default { else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds else if (pageName === 'email') return this.$strings.HeaderEmail + else if (pageName === 'authentication') return this.$strings.HeaderAuthentication } return this.$strings.HeaderSettings } diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue new file mode 100644 index 000000000..acc0ac130 --- /dev/null +++ b/client/pages/config/authentication.vue @@ -0,0 +1,138 @@ + + + + diff --git a/client/store/index.js b/client/store/index.js index 2f8201c16..ed7c35b61 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -66,7 +66,7 @@ export const getters = { export const actions = { updateServerSettings({ commit }, payload) { - var updatePayload = { + const updatePayload = { ...payload } return this.$axios.$patch('/api/settings', updatePayload).then((result) => { diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 75606da29..47cfe4480 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -88,6 +88,7 @@ "HeaderAppriseNotificationSettings": "Apprise Notification Settings", "HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudioTracks": "Audio Tracks", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Backups", "HeaderChangePassword": "Change Password", "HeaderChapters": "Chapters", diff --git a/server/Auth.js b/server/Auth.js index 0041fbed0..d6d67d491 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -57,24 +57,23 @@ class Auth { userInfoURL: global.ServerSettings.authOpenIDUserInfoURL, clientID: global.ServerSettings.authOpenIDClientID, clientSecret: global.ServerSettings.authOpenIDClientSecret, - callbackURL: global.ServerSettings.authOpenIDCallbackURL, + callbackURL: '/auth/openid/callback', scope: ["openid", "email", "profile"], skipUserProfile: false - }, - (async function (issuer, profile, done) { - // TODO: do we want to create the users which does not exist? + }, async (issuer, profile, done) => { + // TODO: do we want to create the users which does not exist? - const user = await Database.userModel.getUserByUsername(profile.username) + const user = await Database.userModel.getUserByUsername(profile.username) - if (!user?.isActive) { - // deny login - done(null, null) - return - } + if (!user?.isActive) { + // deny login + done(null, null) + return + } - // permit login - return done(null, user) - }).bind(this))) + // permit login + return done(null, user) + })) } // Load the JwtStrategy (always) -> for bearer token auth @@ -111,14 +110,13 @@ class Auth { * @param {import('express').Response} res */ paramsToCookies(req, res) { - if (req.query.isRest && req.query.isRest.toLowerCase() == "true") { + if (req.query.isRest?.toLowerCase() == "true") { // store the isRest flag to the is_rest cookie res.cookie('is_rest', req.query.isRest.toLowerCase(), { maxAge: 120000, // 2 min httpOnly: true }) - } - else { + } else { // no isRest-flag set -> set is_rest cookie to false res.cookie('is_rest', "false", { maxAge: 120000, // 2 min @@ -126,7 +124,7 @@ class Auth { }) // check if we are missing a callback parameter - we need one if isRest=false - if (!req.query.callback || req.query.callback === "") { + if (!req.query.callback) { res.status(400).send({ message: 'No callback parameter' }) @@ -151,19 +149,17 @@ class Auth { // get userLogin json (information about the user, server and the session) const data_json = await this.getUserLoginResponsePayload(req.user) - if (req.cookies.is_rest && req.cookies.is_rest === "true") { + if (req.cookies.is_rest === 'true') { // REST request - send data res.json(data_json) - } - else { + } else { // UI request -> check if we have a callback url // TODO: do we want to somehow limit the values for auth_cb? - if (req.cookies.auth_cb && req.cookies.auth_cb.startsWith("http")) { + if (req.cookies.auth_cb?.startsWith('http')) { // UI request -> redirect to auth_cb url and send the jwt token as parameter res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) - } - else { - res.status(400).send("No callback or already expired") + } else { + res.status(400).send('No callback or already expired') } } } @@ -205,7 +201,7 @@ class Auth { // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', - passport.authenticate('openidconnect'), + passport.authenticate('openidconnect', { failureRedirect: '/login', failureMessage: true }), // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this) ) diff --git a/server/Server.js b/server/Server.js index dbb9bddf3..08f4b8d9b 100644 --- a/server/Server.js +++ b/server/Server.js @@ -163,8 +163,6 @@ class Server { this.server = http.createServer(app) - - router.use(fileUpload({ defCharset: 'utf8', defParamCharset: 'utf8', diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 0fa1c62fe..d1f3686b6 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -117,8 +117,9 @@ class MiscController { /** * PATCH: /api/settings * Update server settings - * @param {*} req - * @param {*} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async updateServerSettings(req, res) { if (!req.user.isAdminOrUp) { @@ -246,8 +247,8 @@ class MiscController { * POST: /api/authorize * Used to authorize an API token * - * @param {*} req - * @param {*} res + * @param {import('express').Request} req + * @param {import('express').Response} res */ async authorize(req, res) { if (!req.user) { @@ -539,5 +540,19 @@ class MiscController { res.status(400).send(error.message) } } + + /** + * GET: api/auth-settings (admin only) + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + getAuthSettings(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get auth settings`) + return res.sendStatus(403) + } + return res.json(Database.serverSettings.authenticationSettings) + } } module.exports = new MiscController() \ No newline at end of file diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 71358a00c..9348d6910 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -64,14 +64,13 @@ class ServerSettings { this.authGoogleOauth20ClientSecret = '' this.authGoogleOauth20CallbackURL = '' - // generic-oauth20 settings + // openid settings this.authOpenIDIssuerURL = '' this.authOpenIDAuthorizationURL = '' this.authOpenIDTokenURL = '' this.authOpenIDUserInfoURL = '' this.authOpenIDClientID = '' this.authOpenIDClientSecret = '' - this.authOpenIDCallbackURL = '' if (settings) { this.construct(settings) @@ -126,7 +125,6 @@ class ServerSettings { this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || '' this.authOpenIDClientID = settings.authOpenIDClientID || '' this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' - this.authOpenIDCallbackURL = settings.authOpenIDCallbackURL || '' if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] @@ -144,16 +142,15 @@ class ServerSettings { // remove uninitialized methods // OpenID - if (this.authActiveAuthMethods.includes('generic-oauth20') && ( + if (this.authActiveAuthMethods.includes('openid') && ( this.authOpenIDIssuerURL === '' || this.authOpenIDAuthorizationURL === '' || this.authOpenIDTokenURL === '' || this.authOpenIDUserInfoURL === '' || this.authOpenIDClientID === '' || - this.authOpenIDClientSecret === '' || - this.authOpenIDCallbackURL === '' + this.authOpenIDClientSecret === '' )) { - this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('generic-oauth20', 0), 1) + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('openid', 0), 1) } // fallback to local @@ -228,8 +225,7 @@ class ServerSettings { authOpenIDTokenURL: this.authOpenIDTokenURL, authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client - authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client - authOpenIDCallbackURL: this.authOpenIDCallbackURL + authOpenIDClientSecret: this.authOpenIDClientSecret // Do not return to client } } @@ -243,13 +239,42 @@ class ServerSettings { return json } + get authenticationSettings() { + return { + authActiveAuthMethods: this.authActiveAuthMethods, + authGoogleOauth20ClientID: this.authGoogleOauth20ClientID, // Do not return to client + authGoogleOauth20ClientSecret: this.authGoogleOauth20ClientSecret, // Do not return to client + authGoogleOauth20CallbackURL: this.authGoogleOauth20CallbackURL, + authOpenIDIssuerURL: this.authOpenIDIssuerURL, + authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, + authOpenIDTokenURL: this.authOpenIDTokenURL, + authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, + authOpenIDClientID: this.authOpenIDClientID, // Do not return to client + authOpenIDClientSecret: this.authOpenIDClientSecret // Do not return to client + } + } + + /** + * Update server settings + * + * @param {Object} payload + * @returns {boolean} true if updates were made + */ update(payload) { let hasUpdates = false for (const key in payload) { - if (key === 'sortingPrefixes' && payload[key] && payload[key].length) { - const prefixesCleaned = payload[key].filter(prefix => !!prefix).map(prefix => prefix.toLowerCase()) - if (prefixesCleaned.join(',') !== this[key].join(',')) { - this[key] = [...prefixesCleaned] + if (key === 'sortingPrefixes') { + // Sorting prefixes are updated with the /api/sorting-prefixes endpoint + continue + } else if (key === 'authActiveAuthMethods') { + if (!payload[key]?.length) { + Logger.error(`[ServerSettings] Invalid authActiveAuthMethods`, payload[key]) + continue + } + this.authActiveAuthMethods.sort() + payload[key].sort() + if (payload[key].join() !== this.authActiveAuthMethods.join()) { + this.authActiveAuthMethods = payload[key] hasUpdates = true } } else if (this[key] !== payload[key]) { diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 71d9429e8..d91c93122 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -306,6 +306,7 @@ class ApiRouter { this.router.post('/genres/rename', MiscController.renameGenre.bind(this)) this.router.delete('/genres/:genre', MiscController.deleteGenre.bind(this)) this.router.post('/validate-cron', MiscController.validateCronExpression.bind(this)) + this.router.get('/auth-settings', MiscController.getAuthSettings.bind(this)) } async getDirectories(dir, relpath, excludedDirs, level = 0) { From 0d5a30b21440ad0736332a976cb43eba264112c9 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 25 Sep 2023 17:05:58 -0500 Subject: [PATCH 025/208] Update JWT auth extractors, add state in openid redirect, add back cors for api router --- server/Auth.js | 42 +++++++++++++++++++------ server/Server.js | 2 +- server/controllers/SessionController.js | 20 ++++++------ 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index d6d67d491..b7ea59c4f 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -16,6 +16,18 @@ class Auth { constructor() { } + static cors(req, res, next) { + res.header('Access-Control-Allow-Origin', '*') + res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS') + res.header('Access-Control-Allow-Headers', '*') + res.header('Access-Control-Allow-Credentials', true) + if (req.method === 'OPTIONS') { + res.sendStatus(200) + } else { + next() + } + } + /** * Inializes all passportjs strategies and other passportjs ralated initialization. */ @@ -78,7 +90,7 @@ class Auth { // Load the JwtStrategy (always) -> for bearer token auth passport.use(new JwtStrategy({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]), secretOrKey: Database.serverSettings.tokenSecret }, this.jwtAuthCheck.bind(this))) @@ -123,15 +135,25 @@ class Auth { httpOnly: true }) + // persist state if passed in + if (req.query.state) { + res.cookie('auth_state', req.query.state, { + maxAge: 120000, // 2 min + httpOnly: true + }) + } + + const callback = req.query.redirect_uri || req.query.callback + // check if we are missing a callback parameter - we need one if isRest=false - if (!req.query.callback) { + if (!callback) { res.status(400).send({ message: 'No callback parameter' }) return } // store the callback url to the auth_cb cookie - res.cookie('auth_cb', req.query.callback, { + res.cookie('auth_cb', callback, { maxAge: 120000, // 2 min httpOnly: true }) @@ -155,9 +177,10 @@ class Auth { } else { // UI request -> check if we have a callback url // TODO: do we want to somehow limit the values for auth_cb? - if (req.cookies.auth_cb?.startsWith('http')) { + if (req.cookies.auth_cb) { + let stateQuery = req.cookies.auth_state ? `&state=${req.cookies.auth_state}` : '' // UI request -> redirect to auth_cb url and send the jwt token as parameter - res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`) + res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}${stateQuery}`) } else { res.status(400).send('No callback or already expired') } @@ -201,10 +224,9 @@ class Auth { // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', - passport.authenticate('openidconnect', { failureRedirect: '/login', failureMessage: true }), + passport.authenticate('openidconnect'), // on a successfull login: read the cookies and react like the client requested (callback or json) - this.handleLoginSuccessBasedOnCookie.bind(this) - ) + this.handleLoginSuccessBasedOnCookie.bind(this)) // Logout route router.post('/logout', (req, res) => { @@ -288,9 +310,9 @@ class Auth { */ async jwtAuthCheck(jwt_payload, done) { // load user by id from the jwt token - const user = await Database.userModel.getUserById(jwt_payload.id) + const user = await Database.userModel.getUserByIdOrOldId(jwt_payload.userId) - if (!user || !user.isActive) { + if (!user?.isActive) { // deny login done(null, null) return diff --git a/server/Server.js b/server/Server.js index 08f4b8d9b..2f04b850c 100644 --- a/server/Server.js +++ b/server/Server.js @@ -180,7 +180,7 @@ class Server { router.use(express.static(Path.join(global.appRoot, 'static'))) // router.use('/api/v1', routes) // TODO: New routes - router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router) + router.use('/api', Auth.cors, this.authMiddleware.bind(this), this.apiRouter.router) router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router) // RSS Feed temp route diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 85baeb273..884f0cd69 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -6,7 +6,7 @@ class SessionController { constructor() { } async findOne(req, res) { - return res.json(req.session) + return res.json(req.playbackSession) } async getAllWithUserData(req, res) { @@ -63,32 +63,32 @@ class SessionController { } async getOpenSession(req, res) { - const libraryItem = await Database.libraryItemModel.getOldById(req.session.libraryItemId) - const sessionForClient = req.session.toJSONForClient(libraryItem) + const libraryItem = await Database.libraryItemModel.getOldById(req.playbackSession.libraryItemId) + const sessionForClient = req.playbackSession.toJSONForClient(libraryItem) res.json(sessionForClient) } // POST: api/session/:id/sync sync(req, res) { - this.playbackSessionManager.syncSessionRequest(req.user, req.session, req.body, res) + this.playbackSessionManager.syncSessionRequest(req.user, req.playbackSession, req.body, res) } // POST: api/session/:id/close close(req, res) { let syncData = req.body if (syncData && !Object.keys(syncData).length) syncData = null - this.playbackSessionManager.closeSessionRequest(req.user, req.session, syncData, res) + this.playbackSessionManager.closeSessionRequest(req.user, req.playbackSession, syncData, res) } // DELETE: api/session/:id async delete(req, res) { // if session is open then remove it - const openSession = this.playbackSessionManager.getSession(req.session.id) + const openSession = this.playbackSessionManager.getSession(req.playbackSession.id) if (openSession) { - await this.playbackSessionManager.removeSession(req.session.id) + await this.playbackSessionManager.removeSession(req.playbackSession.id) } - await Database.removePlaybackSession(req.session.id) + await Database.removePlaybackSession(req.playbackSession.id) res.sendStatus(200) } @@ -111,7 +111,7 @@ class SessionController { return res.sendStatus(404) } - req.session = playbackSession + req.playbackSession = playbackSession next() } @@ -130,7 +130,7 @@ class SessionController { return res.sendStatus(403) } - req.session = playbackSession + req.playbackSession = playbackSession next() } } From 1d3ad38187708ca0c6efefce2d04b82820f19522 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sat, 30 Sep 2023 18:08:03 +0000 Subject: [PATCH 026/208] [cleanup] refactor OpenLib sort into getOpenLibResult --- server/finders/BookFinder.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 96735cc95..debac709f 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -136,6 +136,10 @@ class BookFinder { if (!booksFiltered.length && books.length) { if (this.verbose) Logger.debug(`Search has ${books.length} matches, but no close title matches`) } + booksFiltered.sort((a, b) => { + return a.totalDistance - b.totalDistance + }) + return booksFiltered } @@ -282,12 +286,6 @@ class BookFinder { } } - if (provider === 'openlibrary') { - books.sort((a, b) => { - return a.totalDistance - b.totalDistance - }) - } - return books } From 46b0b3a6efb7f31ac7d67ee5fff6dcbd2ff28542 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 1 Oct 2023 08:42:47 +0000 Subject: [PATCH 027/208] [cleanup] Refactor candidates logic to separate class --- server/finders/BookFinder.js | 113 ++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 47 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index debac709f..b30510f22 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -183,35 +183,67 @@ class BookFinder { return books } - addTitleCandidate(title, candidates) { - // Main variant - const cleanTitle = this.cleanTitleForCompares(title).trim() - if (!cleanTitle) return - candidates.add(cleanTitle) + static TitleCandidates = class { - let candidate = cleanTitle + constructor(bookFinder, cleanAuthor) { + this.bookFinder = bookFinder + this.candidates = new Set() + this.cleanAuthor = cleanAuthor + } - // Remove subtitle - candidate = candidate.replace(/([,:;_]| by ).*/g, "").trim() - if (candidate) - candidates.add(candidate) + add(title) { + const titleTransformers = [ + [/([,:;_]| by ).*/g, ''], // Remove subtitle + [/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers + [/(^| )\d+k(bps)?( |$)/, ' '], // Remove bitrate + [/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''] // Remove edition + ] - // Remove preceding/trailing numbers - candidate = candidate.replace(/^\d+ | \d+$/g, "").trim() - if (candidate) - candidates.add(candidate) + // Main variant + const cleanTitle = this.bookFinder.cleanTitleForCompares(title).trim() + if (!cleanTitle) return + this.candidates.add(cleanTitle) - // Remove bitrate - candidate = candidate.replace(/(^| )\d+k(bps)?( |$)/, " ").trim() - if (candidate) - candidates.add(candidate) + let candidate = cleanTitle - // Remove edition - candidate = candidate.replace(/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/, "").trim() - if (candidate) - candidates.add(candidate) + for (const transformer of titleTransformers) { + candidate = candidate.replace(transformer[0], transformer[1]).trim() + if (candidate) { + this.candidates.add(candidate) + } + } + } + + get size() { + return this.candidates.size + } + + getCandidates() { + var candidates = [...this.candidates] + candidates.sort((a, b) => { + // Candidates that include the author are likely low quality + const includesAuthorDiff = !b.includes(this.cleanAuthor) - !a.includes(this.cleanAuthor) + if (includesAuthorDiff) return includesAuthorDiff + // Candidates that include only digits are also likely low quality + const onlyDigits = /^\d+$/ + const includesOnlyDigitsDiff = !onlyDigits.test(b) - !onlyDigits.test(a) + if (includesOnlyDigitsDiff) return includesOnlyDigitsDiff + // Start with longer candidaets, as they are likely more specific + const lengthDiff = b.length - a.length + if (lengthDiff) return lengthDiff + return b.localeCompare(a) + }) + Logger.debug(`[${this.constructor.name}] Found ${candidates.length} fuzzy title candidates`) + Logger.debug(candidates) + return candidates + } + + delete(title) { + return this.candidates.delete(title) + } } + /** * Search for books including fuzzy searches * @@ -240,46 +272,33 @@ class BookFinder { title = title.trim().toLowerCase() author = author.trim().toLowerCase() + const cleanAuthor = this.cleanAuthorForCompares(author) + // Now run up to maxFuzzySearches fuzzy searches - let candidates = new Set() - let cleanedAuthor = this.cleanAuthorForCompares(author) - this.addTitleCandidate(title, candidates) + let titleCandidates = new BookFinder.TitleCandidates(this, cleanAuthor) + titleCandidates.add(title) // remove parentheses and their contents, and replace with a separator const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}/g, " - ") // Split title into hypen-separated parts const titleParts = cleanTitle.split(/ - | -|- /) for (const titlePart of titleParts) { - this.addTitleCandidate(titlePart, candidates) + titleCandidates.add(titlePart) } // We already searched for original title - if (author == cleanedAuthor) candidates.delete(title) - if (candidates.size > 0) { - candidates = [...candidates] - candidates.sort((a, b) => { - // Candidates that include the author are likely low quality - const includesAuthorDiff = !b.includes(cleanedAuthor) - !a.includes(cleanedAuthor) - if (includesAuthorDiff) return includesAuthorDiff - // Candidates that include only digits are also likely low quality - const onlyDigits = /^\d+$/ - const includesOnlyDigitsDiff = !onlyDigits.test(b) - !onlyDigits.test(a) - if (includesOnlyDigitsDiff) return includesOnlyDigitsDiff - // Start with longer candidaets, as they are likely more specific - const lengthDiff = b.length - a.length - if (lengthDiff) return lengthDiff - return b.localeCompare(a) - }) - Logger.debug(`[BookFinder] Found ${candidates.length} fuzzy title candidates`, candidates) - for (const candidate of candidates) { + if (author == cleanAuthor) titleCandidates.delete(title) + if (titleCandidates.size > 0) { + titleCandidates = titleCandidates.getCandidates() + for (const titleCandidate of titleCandidates) { if (++numFuzzySearches > maxFuzzySearches) return books - books = await this.runSearch(candidate, cleanedAuthor, provider, asin, maxTitleDistance, maxAuthorDistance) + books = await this.runSearch(titleCandidate, cleanAuthor, provider, asin, maxTitleDistance, maxAuthorDistance) if (books.length) break } if (!books.length) { // Now try searching without the author - for (const candidate of candidates) { + for (const titleCandidate of titleCandidates) { if (++numFuzzySearches > maxFuzzySearches) return books - books = await this.runSearch(candidate, '', provider, asin, maxTitleDistance, maxAuthorDistance) + books = await this.runSearch(titleCandidate, '', provider, asin, maxTitleDistance, maxAuthorDistance) if (books.length) break } } From 733ad52684cfa0b88e6d7aa33d31a01f87f374e3 Mon Sep 17 00:00:00 2001 From: MarshDeer Date: Mon, 2 Oct 2023 19:42:25 -0300 Subject: [PATCH 028/208] Typo correction --- client/strings/en-us.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/en-us.json b/client/strings/en-us.json index ae018d4bb..158127662 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -417,7 +417,7 @@ "LabelSettingsPreferAudioMetadata": "Prefer audio metadata", "LabelSettingsPreferAudioMetadataHelp": "Audio file ID3 meta tags will be used for book details over folder names", "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata", - "LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.", + "LabelSettingsPreferMatchedMetadataHelp": "Matched data will override item details when using Quick Match. By default Quick Match will only fill in missing details.", "LabelSettingsPreferOPFMetadata": "Prefer OPF metadata", "LabelSettingsPreferOPFMetadataHelp": "OPF file metadata will be used for book details over folder names", "LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN", @@ -713,4 +713,4 @@ "ToastSocketFailedToConnect": "Socket failed to connect", "ToastUserDeleteFailed": "Failed to delete user", "ToastUserDeleteSuccess": "User deleted" -} \ No newline at end of file +} From 8e97be8ef41fc3f11aa4b9faf07eb1ae9bda151c Mon Sep 17 00:00:00 2001 From: MarshDeer Date: Mon, 2 Oct 2023 19:42:42 -0300 Subject: [PATCH 029/208] Spanish translation completed --- client/strings/es.json | 454 ++++++++++++++++++++--------------------- 1 file changed, 227 insertions(+), 227 deletions(-) diff --git a/client/strings/es.json b/client/strings/es.json index b872ca4e4..d2ea30c75 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -20,11 +20,11 @@ "ButtonCreate": "Crear", "ButtonCreateBackup": "Crear Respaldo", "ButtonDelete": "Eliminar", - "ButtonDownloadQueue": "Queue", + "ButtonDownloadQueue": "Fila", "ButtonEdit": "Editar", - "ButtonEditChapters": "Editar Capitulo", + "ButtonEditChapters": "Editar Capítulo", "ButtonEditPodcast": "Editar Podcast", - "ButtonForceReScan": "Forzar Re-Escanear", + "ButtonForceReScan": "Forzar Re-Escaneo", "ButtonFullPath": "Ruta de Acceso Completa", "ButtonHide": "Esconder", "ButtonHome": "Inicio", @@ -34,13 +34,13 @@ "ButtonLogout": "Cerrar Sesión", "ButtonLookup": "Buscar", "ButtonManageTracks": "Administrar Pistas de Audio", - "ButtonMapChapterTitles": "Map Chapter Titles", + "ButtonMapChapterTitles": "Asignar Títulos a Capítulos", "ButtonMatchAllAuthors": "Encontrar Todos los Autores", "ButtonMatchBooks": "Encontrar Libros", "ButtonNevermind": "Olvidar", "ButtonOk": "Ok", "ButtonOpenFeed": "Abrir Fuente", - "ButtonOpenManager": "Open Manager", + "ButtonOpenManager": "Abrir Editor", "ButtonPlay": "Reproducir", "ButtonPlaying": "Reproduciendo", "ButtonPlaylists": "Listas de Reproducción", @@ -55,7 +55,7 @@ "ButtonRemoveAll": "Remover Todos", "ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca", "ButtonRemoveFromContinueListening": "Remover de Continuar Escuchando", - "ButtonRemoveFromContinueReading": "Remove from Continue Reading", + "ButtonRemoveFromContinueReading": "Remover de Continuar Leyendo", "ButtonRemoveSeriesFromContinueSeries": "Remover Serie de Continuar Series", "ButtonReScan": "Re-Escanear", "ButtonReset": "Reiniciar", @@ -74,7 +74,7 @@ "ButtonStartM4BEncode": "Iniciar Codificación M4B", "ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata", "ButtonSubmit": "Enviar", - "ButtonTest": "Test", + "ButtonTest": "Prueba", "ButtonUpload": "Subir", "ButtonUploadBackup": "Subir Respaldo", "ButtonUploadCover": "Subir Portada", @@ -98,12 +98,12 @@ "HeaderCurrentDownloads": "Descargando Actualmente", "HeaderDetails": "Detalles", "HeaderDownloadQueue": "Lista de Descarga", - "HeaderEbookFiles": "Ebook Files", + "HeaderEbookFiles": "Archivos de Ebook", "HeaderEmail": "Email", - "HeaderEmailSettings": "Email Settings", + "HeaderEmailSettings": "Opciones de Email", "HeaderEpisodes": "Episodios", - "HeaderEreaderDevices": "Ereader Devices", - "HeaderEreaderSettings": "Ereader Settings", + "HeaderEreaderDevices": "Dispositivos Ereader", + "HeaderEreaderSettings": "Opciones de Ereader", "HeaderFiles": "Elemento", "HeaderFindChapters": "Buscar Capitulo", "HeaderIgnoredFiles": "Ignorar Elemento", @@ -120,7 +120,7 @@ "HeaderLogs": "Logs", "HeaderManageGenres": "Administrar Géneros", "HeaderManageTags": "Administrar Etiquetas", - "HeaderMapDetails": "Map details", + "HeaderMapDetails": "Asignar Detalles", "HeaderMatch": "Encontrar", "HeaderMetadataToEmbed": "Metadatos para Insertar", "HeaderNewAccount": "Nueva Cuenta", @@ -129,7 +129,7 @@ "HeaderOpenRSSFeed": "Abrir fuente RSS", "HeaderOtherFiles": "Otros Archivos", "HeaderPermissions": "Permisos", - "HeaderPlayerQueue": "Player Queue", + "HeaderPlayerQueue": "Fila del Reproductor", "HeaderPlaylist": "Lista de Reproducción", "HeaderPlaylistItems": "Elementos de Lista de Reproducción", "HeaderPodcastsToAdd": "Podcasts para agregar", @@ -139,13 +139,13 @@ "HeaderRSSFeedGeneral": "Detalles RSS", "HeaderRSSFeedIsOpen": "Fuente RSS esta abierta", "HeaderRSSFeeds": "RSS Feeds", - "HeaderSavedMediaProgress": "Guardar Progreso de multimedia", + "HeaderSavedMediaProgress": "Guardar Progreso de Multimedia", "HeaderSchedule": "Horario", "HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca", "HeaderSession": "Session", "HeaderSetBackupSchedule": "Programar Respaldo", "HeaderSettings": "Configuraciones", - "HeaderSettingsDisplay": "Display", + "HeaderSettingsDisplay": "Interfaz", "HeaderSettingsExperimental": "Funciones Experimentales", "HeaderSettingsGeneral": "General", "HeaderSettingsScanner": "Escáner", @@ -156,21 +156,21 @@ "HeaderStatsRecentSessions": "Sesiones Recientes", "HeaderStatsTop10Authors": "Top 10 Autores", "HeaderStatsTop5Genres": "Top 5 Géneros", - "HeaderTableOfContents": "Table of Contents", + "HeaderTableOfContents": "Tabla de Contenidos", "HeaderTools": "Herramientas", "HeaderUpdateAccount": "Actualizar Cuenta", "HeaderUpdateAuthor": "Actualizar Autor", "HeaderUpdateDetails": "Actualizar Detalles", "HeaderUpdateLibrary": "Actualizar Biblioteca", "HeaderUsers": "Usuarios", - "HeaderYourStats": "Tus Estáticas", - "LabelAbridged": "Abridged", + "HeaderYourStats": "Tus Estadísticas", + "LabelAbridged": "Abreviado", "LabelAccountType": "Tipo de Cuenta", "LabelAccountTypeAdmin": "Administrador", "LabelAccountTypeGuest": "Invitado", "LabelAccountTypeUser": "Usuario", "LabelActivity": "Actividad", - "LabelAdded": "Added", + "LabelAdded": "Añadido", "LabelAddedAt": "Añadido", "LabelAddToCollection": "Añadido a la Colección", "LabelAddToCollectionBatch": "Se Añadieron {0} Libros a la Colección", @@ -186,38 +186,38 @@ "LabelAuthors": "Autores", "LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente", "LabelBackToUser": "Regresar a Usuario", - "LabelBackupLocation": "Backup Location", + "LabelBackupLocation": "Ubicación del Respaldo", "LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático", "LabelBackupsEnableAutomaticBackupsHelp": "Respaldo Guardado en /metadata/backups", "LabelBackupsMaxBackupSize": "Tamaño Máximo de Respaldos (en GB)", - "LabelBackupsMaxBackupSizeHelp": "Como protección contra una configuración errónea, los respaldos fallaran si se excede el tamaño configurado.", + "LabelBackupsMaxBackupSizeHelp": "Como protección contra una configuración errónea, los respaldos fallarán si se excede el tamaño configurado.", "LabelBackupsNumberToKeep": "Numero de respaldos para conservar", - "LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, necesita removerlos manualmente.", + "LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.", "LabelBitrate": "Bitrate", "LabelBooks": "Libros", "LabelChangePassword": "Cambiar Contraseña", "LabelChannels": "Canales", - "LabelChapters": "Capitulos", - "LabelChaptersFound": "Capitulo Encontrado", - "LabelChapterTitle": "Titulo del Capitulo", - "LabelClosePlayer": "Close player", + "LabelChapters": "Capítulos", + "LabelChaptersFound": "Capítulo Encontrado", + "LabelChapterTitle": "Titulo del Capítulo", + "LabelClosePlayer": "Cerrar Reproductor", "LabelCodec": "Codec", - "LabelCollapseSeries": "Colapsar Series", - "LabelCollection": "Collection", + "LabelCollapseSeries": "Colapsar Serie", + "LabelCollection": "Colección", "LabelCollections": "Colecciones", "LabelComplete": "Completo", "LabelConfirmPassword": "Confirmar Contraseña", "LabelContinueListening": "Continuar Escuchando", - "LabelContinueReading": "Continue Reading", - "LabelContinueSeries": "Continuar Series", + "LabelContinueReading": "Continuar Leyendo", + "LabelContinueSeries": "Continuar Serie", "LabelCover": "Portada", "LabelCoverImageURL": "URL de Imagen de Portada", "LabelCreatedAt": "Creado", - "LabelCronExpression": "Cron Expression", + "LabelCronExpression": "Expresión de Cron", "LabelCurrent": "Actual", "LabelCurrently": "En este momento:", - "LabelCustomCronExpression": "Custom Cron Expression:", - "LabelDatetime": "Datetime", + "LabelCustomCronExpression": "Expresión de Cron Personalizada:", + "LabelDatetime": "Hora y Fecha", "LabelDescription": "Descripción", "LabelDeselectAll": "Deseleccionar Todos", "LabelDevice": "Dispositivo", @@ -225,19 +225,19 @@ "LabelDirectory": "Directorio", "LabelDiscFromFilename": "Disco a partir del Nombre del Archivo", "LabelDiscFromMetadata": "Disco a partir de Metadata", - "LabelDiscover": "Discover", + "LabelDiscover": "Descubrir", "LabelDownload": "Descargar", - "LabelDownloadNEpisodes": "Download {0} episodes", + "LabelDownloadNEpisodes": "Descargar {0} episodios", "LabelDuration": "Duración", "LabelDurationFound": "Duración Comprobada:", "LabelEbook": "Ebook", "LabelEbooks": "Ebooks", "LabelEdit": "Editar", "LabelEmail": "Email", - "LabelEmailSettingsFromAddress": "From Address", - "LabelEmailSettingsSecure": "Secure", - "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)", - "LabelEmailSettingsTestAddress": "Test Address", + "LabelEmailSettingsFromAddress": "Remitente", + "LabelEmailSettingsSecure": "Seguridad", + "LabelEmailSettingsSecureHelp": "Si está activado, se usará TLS para conectarse al servidor. Si está apagado, se usará TLS si su servidor tiene soporte para la extensión STARTTLS. En la mayoría de los casos, puede dejar esta opción activada si se está conectando al puerto 465. Apáguela en el caso de usar los puertos 587 o 25. (de nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Probar Dirección", "LabelEmbeddedCover": "Portada Integrada", "LabelEnable": "Habilitar", "LabelEnd": "Fin", @@ -256,13 +256,13 @@ "LabelFinished": "Terminado", "LabelFolder": "Carpeta", "LabelFolders": "Carpetas", - "LabelFontScale": "Font scale", + "LabelFontScale": "Tamaño de Fuente", "LabelFormat": "Formato", "LabelGenre": "Genero", "LabelGenres": "Géneros", "LabelHardDeleteFile": "Eliminar Definitivamente", - "LabelHasEbook": "Has ebook", - "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHasEbook": "Tiene Ebook", + "LabelHasSupplementaryEbook": "Tiene Ebook Suplementario", "LabelHost": "Host", "LabelHour": "Hora", "LabelIcon": "Icono", @@ -276,34 +276,34 @@ "LabelIntervalEvery2Hours": "Cada 2 Horas", "LabelIntervalEvery30Minutes": "Cada 30 minutos", "LabelIntervalEvery6Hours": "Cada 6 Horas", - "LabelIntervalEveryDay": "Cada Dia", + "LabelIntervalEveryDay": "Cada Día", "LabelIntervalEveryHour": "Cada Hora", - "LabelInvalidParts": "Partes Invalidas", - "LabelInvert": "Invert", + "LabelInvalidParts": "Partes Inválidas", + "LabelInvert": "Invertir", "LabelItem": "Elemento", "LabelLanguage": "Lenguaje", "LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor", - "LabelLastBookAdded": "Last Book Added", - "LabelLastBookUpdated": "Last Book Updated", - "LabelLastSeen": "Ultima Vez Visto", - "LabelLastTime": "Ultima Vez", - "LabelLastUpdate": "Ultima Actualización", - "LabelLayout": "Layout", - "LabelLayoutSinglePage": "Single page", - "LabelLayoutSplitPage": "Split page", + "LabelLastBookAdded": "Último Libro Agregado", + "LabelLastBookUpdated": "Último Libro Actualizado", + "LabelLastSeen": "Última Vez Visto", + "LabelLastTime": "Última Vez", + "LabelLastUpdate": "Última Actualización", + "LabelLayout": "Distribución", + "LabelLayoutSinglePage": "Una Página", + "LabelLayoutSplitPage": "Dos Páginas", "LabelLess": "Menos", "LabelLibrariesAccessibleToUser": "Bibliotecas Disponibles para el Usuario", "LabelLibrary": "Biblioteca", "LabelLibraryItem": "Elemento de Biblioteca", "LabelLibraryName": "Nombre de Biblioteca", "LabelLimit": "Limites", - "LabelLineSpacing": "Line spacing", + "LabelLineSpacing": "Interlineado", "LabelListenAgain": "Escuchar Otra Vez", "LabelLogLevelDebug": "Debug", - "LabelLogLevelInfo": "Info", + "LabelLogLevelInfo": "Información", "LabelLogLevelWarn": "Advertencia", "LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha", - "LabelMediaPlayer": "Media Player", + "LabelMediaPlayer": "Reproductor de Medios", "LabelMediaType": "Tipo de Multimedia", "LabelMetadataProvider": "Proveedor de Metadata", "LabelMetaTag": "Meta Tag", @@ -311,8 +311,8 @@ "LabelMinute": "Minuto", "LabelMissing": "Ausente", "LabelMissingParts": "Partes Ausentes", - "LabelMore": "Mas", - "LabelMoreInfo": "Mas Información", + "LabelMore": "Más", + "LabelMoreInfo": "Más Información", "LabelName": "Nombre", "LabelNarrator": "Narrador", "LabelNarrators": "Narradores", @@ -322,17 +322,17 @@ "LabelNewPassword": "Nueva Contraseña", "LabelNextBackupDate": "Fecha del Siguiente Respaldo", "LabelNextScheduledRun": "Próxima Ejecución Programada", - "LabelNoEpisodesSelected": "No episodes selected", + "LabelNoEpisodesSelected": "Ningún Episodio Seleccionado", "LabelNotes": "Notas", "LabelNotFinished": "No Terminado", - "LabelNotificationAppriseURL": "Apprise URL(s)", + "LabelNotificationAppriseURL": "URL(s) de Apprise", "LabelNotificationAvailableVariables": "Variables Disponibles", "LabelNotificationBodyTemplate": "Plantilla de Cuerpo", "LabelNotificationEvent": "Evento de Notificación", "LabelNotificationsMaxFailedAttempts": "Máximo de Intentos Fallidos", - "LabelNotificationsMaxFailedAttemptsHelp": "Las notificaciones se desactivan después de fallar este numero de veces", - "LabelNotificationsMaxQueueSize": "Tamaño máximo de la cola de notificación", - "LabelNotificationsMaxQueueSizeHelp": "Las notificaciones están limitadas a 1 por segundo. Las notificaciones serán ignorados si llegan al numero máximo de cola para prevenir spam de eventos.", + "LabelNotificationsMaxFailedAttemptsHelp": "Las notificaciones se desactivan después de fallar este número de veces", + "LabelNotificationsMaxQueueSize": "Tamaño máximo de la cola de notificaciones", + "LabelNotificationsMaxQueueSizeHelp": "Las notificaciones están limitadas a 1 por segundo. Las notificaciones serán ignoradas si llegan al numero máximo de cola para prevenir spam de eventos.", "LabelNotificationTitleTemplate": "Plantilla de Titulo", "LabelNotStarted": "Sin Iniciar", "LabelNumberOfBooks": "Numero de Libros", @@ -354,97 +354,97 @@ "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", "LabelPodcastType": "Tipo Podcast", - "LabelPort": "Port", + "LabelPort": "Puerto", "LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)", - "LabelPreventIndexing": "Evite que su fuente sea indexado por iTunes y Google podcast directories", - "LabelPrimaryEbook": "Primary ebook", + "LabelPreventIndexing": "Evite que su fuente sea indexada por los directorios de podcasts de iTunes y Google", + "LabelPrimaryEbook": "Ebook principal", "LabelProgress": "Progreso", "LabelProvider": "Proveedor", "LabelPubDate": "Fecha de Publicación", "LabelPublisher": "Editor", "LabelPublishYear": "Año de Publicación", - "LabelRead": "Read", - "LabelReadAgain": "Read Again", - "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", - "LabelRecentlyAdded": "Agregado Reciente", + "LabelRead": "Leído", + "LabelReadAgain": "Volver a leer", + "LabelReadEbookWithoutProgress": "Leer Ebook sin guardar progreso", + "LabelRecentlyAdded": "Agregado Recientemente", "LabelRecentSeries": "Series Recientes", "LabelRecommended": "Recomendados", - "LabelRegion": "Region", + "LabelRegion": "Región", "LabelReleaseDate": "Fecha de Estreno", "LabelRemoveCover": "Remover Portada", - "LabelRSSFeedCustomOwnerEmail": "Custom owner Email", - "LabelRSSFeedCustomOwnerName": "Custom owner Name", + "LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado", + "LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado", "LabelRSSFeedOpen": "Fuente RSS Abierta", - "LabelRSSFeedPreventIndexing": "Prevenir Indaxación", + "LabelRSSFeedPreventIndexing": "Prevenir Indexado", "LabelRSSFeedSlug": "Fuente RSS Slug", "LabelRSSFeedURL": "URL de Fuente RSS", "LabelSearchTerm": "Buscar Termino", "LabelSearchTitle": "Buscar Titulo", - "LabelSearchTitleOrASIN": "Buscar Titulo o ASIN", + "LabelSearchTitleOrASIN": "Buscar Título o ASIN", "LabelSeason": "Temporada", - "LabelSelectAllEpisodes": "Select all episodes", - "LabelSelectEpisodesShowing": "Select {0} episodes showing", - "LabelSendEbookToDevice": "Send Ebook to...", + "LabelSelectAllEpisodes": "Seleccionar todos los episodios", + "LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles", + "LabelSendEbookToDevice": "Enviar Ebook a...", "LabelSequence": "Secuencia", "LabelSeries": "Series", "LabelSeriesName": "Nombre de la Serie", "LabelSeriesProgress": "Progreso de la Serie", - "LabelSetEbookAsPrimary": "Set as primary", - "LabelSetEbookAsSupplementary": "Set as supplementary", - "LabelSettingsAudiobooksOnly": "Audiobooks only", - "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", - "LabelSettingsBookshelfViewHelp": "Diseño Skeumorphic con Estantes de Madera", + "LabelSetEbookAsPrimary": "Establecer como primario", + "LabelSetEbookAsSupplementary": "Establecer como suplementario", + "LabelSettingsAudiobooksOnly": "Sólo Audiolibros", + "LabelSettingsAudiobooksOnlyHelp": "Al activar esta opción se ignorarán los archivos de ebook a menos de que estén dentro de la carpeta de un audiolibro, en cuyo caso se marcarán como ebooks suplementarios", + "LabelSettingsBookshelfViewHelp": "Diseño Esqueuomorfo con Estantes de Madera", "LabelSettingsChromecastSupport": "Soporte para Chromecast", "LabelSettingsDateFormat": "Formato de Fecha", "LabelSettingsDisableWatcher": "Deshabilitar Watcher", "LabelSettingsDisableWatcherForLibrary": "Deshabilitar Watcher de Carpetas para esta biblioteca", - "LabelSettingsDisableWatcherHelp": "Deshabilitar la función automática de agregar/actualizar los elementos, cuando se detecta cambio en los archivos. *Require Reiniciar el Servidor", - "LabelSettingsEnableWatcher": "Enable Watcher", - "LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library", - "LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart", + "LabelSettingsDisableWatcherHelp": "Deshabilitar la función de agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Require Reiniciar el Servidor", + "LabelSettingsEnableWatcher": "Habilitar Watcher", + "LabelSettingsEnableWatcherForLibrary": "Habilitar Watcher para la carpeta de esta biblioteca", + "LabelSettingsEnableWatcherHelp": "Permite agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Requires server restart", "LabelSettingsExperimentalFeatures": "Funciones Experimentales", - "LabelSettingsExperimentalFeaturesHelp": "Funciones en desarrollo sobre las que esperamos sus comentarios y experiencia. Haga click aquí para abrir una conversación en Github.", + "LabelSettingsExperimentalFeaturesHelp": "Funciones en desarrollo que se beneficiarían de sus comentarios y experiencias de prueba. Haga click aquí para abrir una conversación en Github.", "LabelSettingsFindCovers": "Buscar Portadas", - "LabelSettingsFindCoversHelp": "Si tu audiolibro no tiene una portada incluida o la portada no esta dentro de la carpeta, el escaneador tratara de encontrar una portada.
Nota: Esto extenderá el tiempo de escaneo", - "LabelSettingsHideSingleBookSeries": "Hide single book series", - "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", - "LabelSettingsHomePageBookshelfView": "La pagina de inicio usa la vista de librero", - "LabelSettingsLibraryBookshelfView": "La biblioteca usa la vista de librero", - "LabelSettingsOverdriveMediaMarkers": "Usar Markers de multimedia en Overdrive para estos capítulos", - "LabelSettingsOverdriveMediaMarkersHelp": "Archivos MP3 de Overdrive vienen con capítulos con tiempos incrustados como metadata personalizada. Habilitar esto utilizará estas etiquetas para los tiempos de los capítulos automáticamente.", - "LabelSettingsParseSubtitles": "Parse subtitles", - "LabelSettingsParseSubtitlesHelp": "Extraiga subtítulos de los nombres de las carpetas de los audiolibros.
Los subtítulos deben estar separados por \" - \"
ejemplo. \"Titulo Libro - Este Subtitulo\" tiene el subtitulo \"Este Subtitulo\"", - "LabelSettingsPreferAudioMetadata": "Preferir metadata del audio", - "LabelSettingsPreferAudioMetadataHelp": "Archivos de Audio ID3 meta tags se utilizarán para detalles de libros en vez de los nombres de carpetas", - "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata", - "LabelSettingsPreferMatchedMetadataHelp": "Los datos coincidentes anularán los detalles del elemento cuando se utilice Quick Match. Por defecto, Quick Match solo completará los detalles faltantes.", - "LabelSettingsPreferOPFMetadata": "Preferir OPF metadata", - "LabelSettingsPreferOPFMetadataHelp": "Los archivos de metadata OPF serán usados para los detalles del libro en vez de el nombre de las carpetas", + "LabelSettingsFindCoversHelp": "Si tu audiolibro no tiene una portada incluída, o la portada no esta dentro de la carpeta, el escaneador tratará de encontrar una portada.
Nota: Esto extenderá el tiempo de escaneo", + "LabelSettingsHideSingleBookSeries": "Esconder series con un solo libro", + "LabelSettingsHideSingleBookSeriesHelp": "Las series con un solo libro no aparecerán en la página de series ni la repisa para series de la página principal.", + "LabelSettingsHomePageBookshelfView": "Usar la vista de librero en la página principal", + "LabelSettingsLibraryBookshelfView": "Usar la vista de librero en la biblioteca", + "LabelSettingsOverdriveMediaMarkers": "Usar Overdrive Media Markers para estos capítulos", + "LabelSettingsOverdriveMediaMarkersHelp": "Los archivos MP3 de Overdrive vienen con capítulos con tiempos incrustados como metadatos personalizados. Habilitar esta opción utilizará automáticamente estas etiquetas para los tiempos de los capítulos.", + "LabelSettingsParseSubtitles": "Extraer Subtítulos", + "LabelSettingsParseSubtitlesHelp": "Extraer subtítulos de los nombres de las carpetas de los audiolibros.
Los subtítulos deben estar separados por \" - \"
Por ejemplo: \"Ejemplo de Título - Subtítulo Aquí\" tiene el subtítulo \"Subtítulo Aquí\"", + "LabelSettingsPreferAudioMetadata": "Preferir metadatos del archivo de audio", + "LabelSettingsPreferAudioMetadataHelp": "Preferir los metadatos ID3 del archivo de audio en vez de los nombres de carpetas para los detalles de libros", + "LabelSettingsPreferMatchedMetadata": "Preferir metadatos encontrados", + "LabelSettingsPreferMatchedMetadataHelp": "Los datos encontrados sobreescribirán los detalles del elemento cuando se use \"Encontrar Rápido\". Por defecto, \"Encontrar Rápido\" sólo completará los detalles faltantes.", + "LabelSettingsPreferOPFMetadata": "Preferir Metadatos OPF", + "LabelSettingsPreferOPFMetadataHelp": "Preferir los archivos de metadatos OPF en vez de los nombres de carpetas para los detalles de los libros.", "LabelSettingsSkipMatchingBooksWithASIN": "Omitir libros coincidentes que ya tengan un ASIN", "LabelSettingsSkipMatchingBooksWithISBN": "Omitir libros coincidentes que ya tengan un ISBN", - "LabelSettingsSortingIgnorePrefixes": "Ignorar prefijos al ordenando", - "LabelSettingsSortingIgnorePrefixesHelp": "ejemplo. el prefijo \"el\" del titulo \"El titulo del libro\" sera ordenado como \"Titulo del Libro, el\"", + "LabelSettingsSortingIgnorePrefixes": "Ignorar prefijos al ordenar", + "LabelSettingsSortingIgnorePrefixesHelp": "Por ejemplo: El prefijo \"el\" del titulo \"El titulo del libro\" se ordenará como \"Titulo del Libro, el\".", "LabelSettingsSquareBookCovers": "Usar portadas cuadradas", - "LabelSettingsSquareBookCoversHelp": "Prefiere usar portadas cuadradas sobre las portadas estándar 1.6:1", - "LabelSettingsStoreCoversWithItem": "Guardar portada con elemento", - "LabelSettingsStoreCoversWithItemHelp": "Por defecto, las portadas se almacenan en /metadata/items, si habilita esta configuración, las portadas se almacenará en la carpeta de elementos de su biblioteca. Solamente un archivo llamado \"cover\" sera guardado.", - "LabelSettingsStoreMetadataWithItem": "Guardar metadata con elemento", - "LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items, si habilita esta configuración, los archivos de metadata se guardaran en la carpeta de elementos de tu biblioteca. Usa la extension .abs", - "LabelSettingsTimeFormat": "Format de Tiempo", + "LabelSettingsSquareBookCoversHelp": "Prefierir usar portadas cuadradas sobre las portadas estándar 1.6:1", + "LabelSettingsStoreCoversWithItem": "Guardar portadas con elementos", + "LabelSettingsStoreCoversWithItemHelp": "Por defecto, las portadas se almacenan en /metadata/items. Si habilita esta opción, las portadas se almacenarán en la carpeta de elementos de su biblioteca. Se guardará un solo archivo llamado \"cover\".", + "LabelSettingsStoreMetadataWithItem": "Guardar metadatos con elementos", + "LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca. Usa la extensión .abs", + "LabelSettingsTimeFormat": "Formato de Tiempo", "LabelShowAll": "Mostrar Todos", "LabelSize": "Tamaño", "LabelSleepTimer": "Temporizador para Dormir", "LabelSlug": "Slug", "LabelStart": "Iniciar", - "LabelStarted": "Indiciado", + "LabelStarted": "Iniciado", "LabelStartedAt": "Iniciado En", "LabelStartTime": "Tiempo de Inicio", "LabelStatsAudioTracks": "Pistas de Audio", "LabelStatsAuthors": "Autores", - "LabelStatsBestDay": "Mejor Dia", + "LabelStatsBestDay": "Mejor Día", "LabelStatsDailyAverage": "Promedio Diario", - "LabelStatsDays": "Dias", - "LabelStatsDaysListened": "Dias Escuchando", + "LabelStatsDays": "Días", + "LabelStatsDaysListened": "Días Escuchando", "LabelStatsHours": "Horas", "LabelStatsInARow": "seguidos", "LabelStatsItemsFinished": "Elementos Terminados", @@ -453,42 +453,42 @@ "LabelStatsMinutesListening": "Minutos Escuchando", "LabelStatsOverallDays": "Total de Dias", "LabelStatsOverallHours": "Total de Horas", - "LabelStatsWeekListening": "Escuchando en la Semana", - "LabelSubtitle": "Subtitulo", - "LabelSupportedFileTypes": "Tipo de Archivos Soportados", + "LabelStatsWeekListening": "Tiempo escuchando en la Semana", + "LabelSubtitle": "Subtítulo", + "LabelSupportedFileTypes": "Tipos de Archivos Soportados", "LabelTag": "Etiqueta", "LabelTags": "Etiquetas", - "LabelTagsAccessibleToUser": "Etiquetas Accessible para el Usuario", - "LabelTagsNotAccessibleToUser": "Tags not Accessible to User", + "LabelTagsAccessibleToUser": "Etiquetas Accessibles al Usuario", + "LabelTagsNotAccessibleToUser": "Etiquetas no Accesibles al Usuario", "LabelTasks": "Tareas Corriendo", - "LabelTheme": "Theme", - "LabelThemeDark": "Dark", - "LabelThemeLight": "Light", + "LabelTheme": "Tema", + "LabelThemeDark": "Oscuro", + "LabelThemeLight": "Claro", "LabelTimeBase": "Time Base", "LabelTimeListened": "Tiempo Escuchando", "LabelTimeListenedToday": "Tiempo Escuchando Hoy", "LabelTimeRemaining": "{0} restante", "LabelTimeToShift": "Tiempo para Cambiar en Segundos", - "LabelTitle": "Titulo", - "LabelToolsEmbedMetadata": "Incorporar Metadata", - "LabelToolsEmbedMetadataDescription": "Incorpora metadata en archivos de audio incluyendo la portada y capítulos.", - "LabelToolsMakeM4b": "Hacer Archivo M4B de Audiolibro", - "LabelToolsMakeM4bDescription": "Generar archivo .M4B de audiolibro con metadata, imágenes de portada y capítulos incorporados.", + "LabelTitle": "Título", + "LabelToolsEmbedMetadata": "Incrustar Metadatos", + "LabelToolsEmbedMetadataDescription": "Incrusta metadatos en los archivos de audio, incluyendo la portada y capítulos.", + "LabelToolsMakeM4b": "Hacer Archivo de Audiolibro M4B", + "LabelToolsMakeM4bDescription": "Generar archivo de audiolibro .M4B con metadatos, imágenes de portada y capítulos incorporados.", "LabelToolsSplitM4b": "Dividir M4B en Archivos MP3", - "LabelToolsSplitM4bDescription": "Dividir M4B en Archivos MP3 y incorporar metadata, images de portada y capítulos.", + "LabelToolsSplitM4bDescription": "Dividir M4B en Archivos MP3 e incorporar metadatos, imágenes de portada y capítulos.", "LabelTotalDuration": "Duración Total", "LabelTotalTimeListened": "Tiempo Total Escuchado", "LabelTrackFromFilename": "Pista desde el Nombre del Archivo", - "LabelTrackFromMetadata": "Pista desde Metadata", + "LabelTrackFromMetadata": "Pista desde Metadatos", "LabelTracks": "Pistas", - "LabelTracksMultiTrack": "Multi-track", - "LabelTracksNone": "No tracks", - "LabelTracksSingleTrack": "Single-track", + "LabelTracksMultiTrack": "Varias pistas", + "LabelTracksNone": "Ninguna pista", + "LabelTracksSingleTrack": "Una pista", "LabelType": "Tipo", - "LabelUnabridged": "Unabridged", + "LabelUnabridged": "No Abreviado", "LabelUnknown": "Desconocido", "LabelUpdateCover": "Actualizar Portada", - "LabelUpdateCoverHelp": "Permitir sobrescribir portadas existentes de los libros seleccionados cuando sean encontrados.", + "LabelUpdateCoverHelp": "Permitir sobrescribir las portadas existentes de los libros seleccionados cuando sean encontradas.", "LabelUpdatedAt": "Actualizado En", "LabelUpdateDetails": "Actualizar Detalles", "LabelUpdateDetailsHelp": "Permitir sobrescribir detalles existentes de los libros seleccionados cuando sean encontrados", @@ -497,79 +497,79 @@ "LabelUseChapterTrack": "Usar pista por capitulo", "LabelUseFullTrack": "Usar pista completa", "LabelUser": "Usuario", - "LabelUsername": "Nombré de Usuario", + "LabelUsername": "Nombre de Usuario", "LabelValue": "Valor", "LabelVersion": "Versión", "LabelViewBookmarks": "Ver Marcadores", "LabelViewChapters": "Ver Capítulos", - "LabelViewQueue": "Ver player queue", + "LabelViewQueue": "Ver Fila del Reproductor", "LabelVolume": "Volumen", - "LabelWeekdaysToRun": "Correr en Dias de la Semana", + "LabelWeekdaysToRun": "Correr en Días de la Semana", "LabelYourAudiobookDuration": "Duración de tu Audiolibro", - "LabelYourBookmarks": "Tus Marcadores Bookmarks", + "LabelYourBookmarks": "Tus Marcadores", "LabelYourPlaylists": "Tus Listas", "LabelYourProgress": "Tu Progreso", - "MessageAddToPlayerQueue": "Agregar a player queue", - "MessageAppriseDescription": "Para usar esta función deberás tener Apprise API corriendo o un API que maneje los mismos resultados.
El Apprise API URL debe tener la misma ruta de archivos que donde se envina las notificaciones, ejemplo, si su API esta en http://192.168.1.1:8337 entonces pondría http://192.168.1.1:8337/notify.", - "MessageBackupsDescription": "Los respaldos incluyen, usuarios, el progreso del los usuarios, detalles de los elementos de la biblioteca, configuración del servidor y las imágenes en /metadata/items & /metadata/authors. Los Respaldo NO incluyen ningún archivo guardado en la carpeta de tu biblioteca.", - "MessageBatchQuickMatchDescription": "Quick Match tratara de agregar porta y metadata faltantes de los elementos seleccionados. Habilite la opción de abajo para que Quick Match pueda sobrescribir portadas y/o metadata existentes.", + "MessageAddToPlayerQueue": "Agregar a fila del Reproductor", + "MessageAppriseDescription": "Para usar esta función deberás tener la API de Apprise corriendo o una API que maneje los mismos resultados.
La URL de la API de Apprise debe tener la misma ruta de archivos que donde se envían las notificaciones. Por ejemplo: si su API esta en http://192.168.1.1:8337 entonces pondría http://192.168.1.1:8337/notify.", + "MessageBackupsDescription": "Los respaldos incluyen: usuarios, el progreso del los usuarios, los detalles de los elementos de la biblioteca, la configuración del servidor y las imágenes en /metadata/items y /metadata/authors. Los Respaldos NO incluyen ningún archivo guardado en la carpeta de tu biblioteca.", + "MessageBatchQuickMatchDescription": "\"Encontrar Rápido\" tratará de agregar portadas y metadatos faltantes de los elementos seleccionados. Habilite la opción de abajo para que \"Encontrar Rápido\" pueda sobrescribir portadas y/o metadatos existentes.", "MessageBookshelfNoCollections": "No tienes ninguna colección.", "MessageBookshelfNoResultsForFilter": "Ningún Resultado para el filtro \"{0}: {1}\"", "MessageBookshelfNoRSSFeeds": "Ninguna Fuente RSS esta abierta", - "MessageBookshelfNoSeries": "No tienes ninguna series", + "MessageBookshelfNoSeries": "No tienes ninguna serie", "MessageChapterEndIsAfter": "El final del capítulo es después del final de su audiolibro.", "MessageChapterErrorFirstNotZero": "El primer capitulo debe iniciar en 0", - "MessageChapterErrorStartGteDuration": "El tiempo de inicio no es válida debe ser inferior a la duración del audiolibro.", - "MessageChapterErrorStartLtPrev": "El tiempo de inicio no es válida debe ser mayor o igual que la hora de inicio del capítulo anterior", + "MessageChapterErrorStartGteDuration": "El tiempo de inicio no es válido: debe ser inferior a la duración del audiolibro.", + "MessageChapterErrorStartLtPrev": "El tiempo de inicio no es válido: debe ser mayor o igual que el tiempo de inicio del capítulo anterior", "MessageChapterStartIsAfter": "El comienzo del capítulo es después del final de su audiolibro", - "MessageCheckingCron": "Checking cron...", - "MessageConfirmCloseFeed": "Are you sure you want to close this feed?", - "MessageConfirmDeleteBackup": "Esta seguro que desea eliminar el respaldo {0}?", - "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", - "MessageConfirmDeleteLibrary": "Esta seguro que desea eliminar permanentemente la biblioteca \"{0}\"?", - "MessageConfirmDeleteSession": "Esta seguro que desea eliminar esta session?", - "MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?", - "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", - "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", - "MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?", - "MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?", - "MessageConfirmRemoveAllChapters": "Esta seguro que desea remover todos los capitulos?", - "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", - "MessageConfirmRemoveCollection": "Esta seguro que desea remover la colección \"{0}\"?", - "MessageConfirmRemoveEpisode": "Esta seguro que desea remover el episodio \"{0}\"?", - "MessageConfirmRemoveEpisodes": "Esta seguro que desea remover {0} episodios?", - "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", - "MessageConfirmRemovePlaylist": "Esta seguro que desea remover su lista de reproducción \"{0}\"?", - "MessageConfirmRenameGenre": "Esta seguro que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?", - "MessageConfirmRenameGenreMergeNote": "Nota: Este genero ya existe por lo que se fusionarán.", - "MessageConfirmRenameGenreWarning": "Advertencia! un genero similar ya existe \"{0}\".", - "MessageConfirmRenameTag": "Esta seguro que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?", - "MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe por lo que se fusionarán.", + "MessageCheckingCron": "Revisando cron...", + "MessageConfirmCloseFeed": "Está seguro de que desea cerrar esta fuente?", + "MessageConfirmDeleteBackup": "¿Está seguro de que desea eliminar el respaldo {0}?", + "MessageConfirmDeleteFile": "Esto eliminará el archivo de su sistema de archivos. ¿Está seguro?", + "MessageConfirmDeleteLibrary": "¿Está seguro de que desea eliminar permanentemente la biblioteca \"{0}\"?", + "MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?", + "MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?", + "MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?", + "MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?", + "MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?", + "MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?", + "MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?", + "MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?", + "MessageConfirmRemoveCollection": "¿Está seguro de que desea remover la colección \"{0}\"?", + "MessageConfirmRemoveEpisode": "¿Está seguro de que desea remover el episodio \"{0}\"?", + "MessageConfirmRemoveEpisodes": "¿Está seguro de que desea remover {0} episodios?", + "MessageConfirmRemoveNarrator": "¿Está seguro de que desea remover el narrador \"{0}\"?", + "MessageConfirmRemovePlaylist": "¿Está seguro de que desea remover la lista de reproducción \"{0}\"?", + "MessageConfirmRenameGenre": "¿Está seguro de que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?", + "MessageConfirmRenameGenreMergeNote": "Nota: Este género ya existe, por lo que se fusionarán.", + "MessageConfirmRenameGenreWarning": "Advertencia! Un genero similar ya existe \"{0}\".", + "MessageConfirmRenameTag": "¿Está seguro de que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?", + "MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe, por lo que se fusionarán.", "MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".", - "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", + "MessageConfirmSendEbookToDevice": "¿Está seguro de que enviar {0} ebook(s) \"{1}\" al dispositivo \"{2}\"?", "MessageDownloadingEpisode": "Descargando Capitulo", - "MessageDragFilesIntoTrackOrder": "Arrastras los archivos en el orden correcto de la pista.", - "MessageEmbedFinished": "Incorporación Terminada!", + "MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas.", + "MessageEmbedFinished": "Incrustación Terminada!", "MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar", - "MessageFeedURLWillBe": "Fuente URL sera {0}", + "MessageFeedURLWillBe": "URL de la fuente será {0}", "MessageFetching": "Buscando...", - "MessageForceReScanDescription": "Escaneara todos los archivos como un nuevo escaneo. Archivos de audio con etiqueta ID3, archivos OPF y archivos de texto serán escaneados como nuevos.", - "MessageImportantNotice": "Noticia importante!", + "MessageForceReScanDescription": "Escaneará todos los archivos como un nuevo escaneo. Archivos de audio con etiquetas ID3, archivos OPF y archivos de texto serán escaneados como nuevos.", + "MessageImportantNotice": "¡Notificación importante!", "MessageInsertChapterBelow": "Insertar Capítulo Abajo", "MessageItemsSelected": "{0} Elementos Seleccionados", "MessageItemsUpdated": "{0} Elementos Actualizados", - "MessageJoinUsOn": "Únete en", - "MessageListeningSessionsInTheLastYear": "{0} sesiones de escuchadas en el último año", + "MessageJoinUsOn": "Únetenos en", + "MessageListeningSessionsInTheLastYear": "{0} sesiones de escucha en el último año", "MessageLoading": "Cargando...", "MessageLoadingFolders": "Cargando archivos...", - "MessageM4BFailed": "M4B Fallo!", - "MessageM4BFinished": "M4B Terminado!", - "MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps", - "MessageMarkAllEpisodesFinished": "Mark all episodes finished", - "MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished", + "MessageM4BFailed": "¡Fallo de M4B!", + "MessageM4BFinished": "¡M4B Terminado!", + "MessageMapChapterTitles": "Asignar los nombres de capítulos a los capítulos existentes en tu audiolibro sin ajustar sus tiempos", + "MessageMarkAllEpisodesFinished": "Marcar todos los episodios como terminados", + "MessageMarkAllEpisodesNotFinished": "Marcar todos los episodios como no terminados", "MessageMarkAsFinished": "Marcar como Terminado", "MessageMarkAsNotFinished": "Marcar como No Terminado", - "MessageMatchBooksDescription": "intentará hacer coincidir los libros de la biblioteca con un libro del proveedor de búsqueda seleccionado y rellenará los detalles vacíos y la portada. No sobrescribe los detalles.", + "MessageMatchBooksDescription": "Se intentará hacer coincidir los libros de la biblioteca con un libro del proveedor de búsqueda seleccionado, y se rellenarán los detalles vacíos y la portada. No sobrescribe los detalles.", "MessageNoAudioTracks": "Sin Pista de Audio", "MessageNoAuthors": "Sin Autores", "MessageNoBackups": "Sin Respaldos", @@ -582,18 +582,18 @@ "MessageNoDownloadsQueued": "Sin Lista de Descarga", "MessageNoEpisodeMatchesFound": "No se encontraron episodios que coinciden", "MessageNoEpisodes": "Sin Episodios", - "MessageNoFoldersAvailable": "No Carpetas Disponibles", + "MessageNoFoldersAvailable": "No Hay Carpetas Disponibles", "MessageNoGenres": "Sin Géneros", "MessageNoIssues": "Sin Problemas", "MessageNoItems": "Sin Elementos", "MessageNoItemsFound": "Ningún Elemento Encontrado", "MessageNoListeningSessions": "Ninguna Session Escuchada", - "MessageNoLogs": "No Logs", - "MessageNoMediaProgress": "Multimedia sin Progreso ", + "MessageNoLogs": "No hay logs", + "MessageNoMediaProgress": "Multimedia sin Progreso", "MessageNoNotifications": "Ninguna Notificación", - "MessageNoPodcastsFound": "Ningún podcasts encontrado", + "MessageNoPodcastsFound": "Ningún podcast encontrado", "MessageNoResults": "Sin Resultados", - "MessageNoSearchResultsFor": "No hay resultados de la búsqueda para \"{0}\"", + "MessageNoSearchResultsFor": "No hay resultados para la búsqueda \"{0}\"", "MessageNoSeries": "Sin Series", "MessageNoTags": "Sin Etiquetas", "MessageNoTasksRunning": "Ninguna Tarea Corriendo", @@ -603,45 +603,45 @@ "MessageNoUserPlaylists": "No tienes lista de reproducciones", "MessageOr": "o", "MessagePauseChapter": "Pausar la reproducción del capítulo", - "MessagePlayChapter": "Escuche para comenzar el capítulo", - "MessagePlaylistCreateFromCollection": "Crear lista de reproducción a partir de colección", - "MessagePodcastHasNoRSSFeedForMatching": "El podcast no tiene una URL de fuente RSS que pueda usar que coincida", - "MessageQuickMatchDescription": "Rellenar detalles de elementos vacíos y portada con los primeros resultados de '{0}'. No sobrescribe los detalles a menos que la configuración 'Prefer matched metadata' del servidor este habilita.", + "MessagePlayChapter": "Escuchar el comienzo del capítulo", + "MessagePlaylistCreateFromCollection": "Crear una lista de reproducción a partir de una colección", + "MessagePodcastHasNoRSSFeedForMatching": "El podcast no tiene una URL de fuente RSS que pueda usar", + "MessageQuickMatchDescription": "Rellenar detalles de elementos vacíos y portada con los primeros resultados de '{0}'. No sobrescribe los detalles a menos que la opción \"Preferir Metadatos Encontrados\" del servidor esté habilitada.", "MessageRemoveChapter": "Remover capítulos", "MessageRemoveEpisodes": "Remover {0} episodio(s)", - "MessageRemoveFromPlayerQueue": "Romover la cola de reporduccion", - "MessageRemoveUserWarning": "Esta seguro que desea eliminar el usuario \"{0}\"?", - "MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuye en", - "MessageResetChaptersConfirm": "Esta seguro que desea reiniciar el capitulo y deshacer los cambios que hiciste?", - "MessageRestoreBackupConfirm": "Esta seguro que desea para restaurar del respaldo creado en", - "MessageRestoreBackupWarning": "Restaurar sobrescribirá toda la base de datos localizada en /config y las imágenes de portadas en /metadata/items & /metadata/authors.

El respaldo no modifica ningún archivo en las carpetas de tu biblioteca. Si ha habilitado la configuración del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, entonces esos no se respaldan o sobrescribe.

Todos los clientes que usen su servidor se actualizarán automáticamente.", + "MessageRemoveFromPlayerQueue": "Romover la cola de reproducción", + "MessageRemoveUserWarning": "¿Está seguro de que desea eliminar el usuario \"{0}\"?", + "MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuya en", + "MessageResetChaptersConfirm": "¿Está seguro de que desea deshacer los cambios y revertir los capítulos a su estado original?", + "MessageRestoreBackupConfirm": "¿Está seguro de que desea para restaurar del respaldo creado en", + "MessageRestoreBackupWarning": "Restaurar sobrescribirá toda la base de datos localizada en /config y las imágenes de portadas en /metadata/items y /metadata/authors.

El respaldo no modifica ningún archivo en las carpetas de su biblioteca. Si ha habilitado la opción del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, esos archivos no se respaldan o sobrescriben.

Todos los clientes que usen su servidor se actualizarán automáticamente.", "MessageSearchResultsFor": "Resultados de la búsqueda de", - "MessageServerCouldNotBeReached": "No se pude establecer la conexión con el servidor", + "MessageServerCouldNotBeReached": "No se pudo establecer la conexión con el servidor", "MessageSetChaptersFromTracksDescription": "Establecer capítulos usando cada archivo de audio como un capítulo y el título del capítulo como el nombre del archivo de audio", "MessageStartPlaybackAtTime": "Iniciar reproducción para \"{0}\" en {1}?", "MessageThinking": "Pensando...", "MessageUploaderItemFailed": "Error al Subir", - "MessageUploaderItemSuccess": "Éxito al Subir!", + "MessageUploaderItemSuccess": "¡Éxito al Subir!", "MessageUploading": "Subiendo...", - "MessageValidCronExpression": "Valid cron expression", - "MessageWatcherIsDisabledGlobally": "Watcher es desactivado globalmente en la configuración del servidor", - "MessageXLibraryIsEmpty": "{0} La biblioteca esta vacía!", - "MessageYourAudiobookDurationIsLonger": "La duración de tu audiolibro es más larga que la duración encontrada", + "MessageValidCronExpression": "Expresión de Cron bálida", + "MessageWatcherIsDisabledGlobally": "El watcher está desactivado globalmente en la configuración del servidor", + "MessageXLibraryIsEmpty": "La biblioteca {0} está vacía!", + "MessageYourAudiobookDurationIsLonger": "La duración de su audiolibro es más larga que la duración encontrada", "MessageYourAudiobookDurationIsShorter": "La duración de su audiolibro es más corta que la duración encontrada", "NoteChangeRootPassword": "El usuario Root es el único usuario que puede no tener una contraseña", - "NoteChapterEditorTimes": "Nota: La hora de inicio del primer capítulo debe permanecer en 0:00 y la hora de inicio del último capítulo no puede exceder la duración de este audiolibro.", - "NoteFolderPicker": "Nota: las carpetas ya asignadas no se mostrarán", - "NoteFolderPickerDebian": "Nota: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.", - "NoteRSSFeedPodcastAppsHttps": "Advertencia: La mayoría de las aplicaciones de podcast requieren que URL de la fuente RSS use HTTPS", - "NoteRSSFeedPodcastAppsPubDate": "Advertencia: 1 o más de tus episodios no tienen fecha de publicación. Algunas aplicaciones de podcast lo requieren.", + "NoteChapterEditorTimes": "Nota: El tiempo de inicio del primer capítulo debe permanecer en 0:00, y el tiempo de inicio del último capítulo no puede exceder la duración del audiolibro.", + "NoteFolderPicker": "Nota: Las carpetas ya asignadas no se mostrarán", + "NoteFolderPickerDebian": "Nota: El selector de archivos no está completamente implementado para instalaciones en Debian. Deberá ingresar la ruta de la carpeta de su biblioteca directamente.", + "NoteRSSFeedPodcastAppsHttps": "Advertencia: La mayoría de las aplicaciones de podcast requieren que la URL de la fuente RSS use HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "Advertencia: 1 o más de sus episodios no tienen fecha de publicación. Algunas aplicaciones de podcast lo requieren.", "NoteUploaderFoldersWithMediaFiles": "Las carpetas con archivos multimedia se manejarán como elementos separados en la biblioteca.", - "NoteUploaderOnlyAudioFiles": "Si subes solamente un archivos de audio, cada archivo se manejara como un audiolibro.", - "NoteUploaderUnsupportedFiles": "Los archivos no soportados se ignoran. Al elegir o soltar una carpeta, los archivos que no estén en una carpeta serán ignorados.", + "NoteUploaderOnlyAudioFiles": "Si sube solamente archivos de audio, cada archivo se manejará como un audiolibro por separado.", + "NoteUploaderUnsupportedFiles": "Se ignorarán los archivos no soportados. Al elegir o arrastrar una carpeta, los archivos que no estén dentro de una subcarpeta serán ignorados.", "PlaceholderNewCollection": "Nuevo nombre de la colección", "PlaceholderNewFolderPath": "Nueva ruta de carpeta", "PlaceholderNewPlaylist": "Nuevo nombre de la lista de reproducción", - "PlaceholderSearch": "Buscando..", - "PlaceholderSearchEpisode": "Search episode..", + "PlaceholderSearch": "Buscar..", + "PlaceholderSearchEpisode": "Buscar Episodio..", "ToastAccountUpdateFailed": "Error al actualizar cuenta", "ToastAccountUpdateSuccess": "Cuenta actualizada", "ToastAuthorImageRemoveFailed": "Error al eliminar la imagen", @@ -657,16 +657,16 @@ "ToastBackupRestoreFailed": "Error al restaurar el respaldo", "ToastBackupUploadFailed": "Error al subir el respaldo", "ToastBackupUploadSuccess": "Respaldo cargado", - "ToastBatchUpdateFailed": "Batch update failed", - "ToastBatchUpdateSuccess": "Batch update success", + "ToastBatchUpdateFailed": "Subida masiva fallida", + "ToastBatchUpdateSuccess": "Subida masiva exitosa", "ToastBookmarkCreateFailed": "Error al crear marcador", - "ToastBookmarkCreateSuccess": "Marca Agregado", + "ToastBookmarkCreateSuccess": "Marcador Agregado", "ToastBookmarkRemoveFailed": "Error al eliminar marcador", "ToastBookmarkRemoveSuccess": "Marcador eliminado", - "ToastBookmarkUpdateFailed": "Error al eliminar el marcador", + "ToastBookmarkUpdateFailed": "Error al actualizar el marcador", "ToastBookmarkUpdateSuccess": "Marcador actualizado", "ToastChaptersHaveErrors": "Los capítulos tienen errores", - "ToastChaptersMustHaveTitles": "Los capítulos tienen que tener titulo", + "ToastChaptersMustHaveTitles": "Los capítulos tienen que tener un título", "ToastCollectionItemsRemoveFailed": "Error al remover elemento(s) de la colección", "ToastCollectionItemsRemoveSuccess": "Elementos(s) removidos de la colección", "ToastCollectionRemoveFailed": "Error al remover la colección", @@ -676,7 +676,7 @@ "ToastItemCoverUpdateFailed": "Error al actualizar la portada del elemento", "ToastItemCoverUpdateSuccess": "Portada del elemento actualizada", "ToastItemDetailsUpdateFailed": "Error al actualizar los detalles del elemento", - "ToastItemDetailsUpdateSuccess": "Detalles de Elemento Actualizados", + "ToastItemDetailsUpdateSuccess": "Detalles del Elemento Actualizados", "ToastItemDetailsUpdateUnneeded": "No se necesitan actualizaciones para los detalles del Elemento", "ToastItemMarkedAsFinishedFailed": "Error al marcar como Terminado", "ToastItemMarkedAsFinishedSuccess": "Elemento marcado como terminado", @@ -684,11 +684,11 @@ "ToastItemMarkedAsNotFinishedSuccess": "Elemento marcado como No Terminado", "ToastLibraryCreateFailed": "Error al crear biblioteca", "ToastLibraryCreateSuccess": "Biblioteca \"{0}\" creada", - "ToastLibraryDeleteFailed": "Error al eliminar la biblioteca", + "ToastLibraryDeleteFailed": "Error al eliminar biblioteca", "ToastLibraryDeleteSuccess": "Biblioteca eliminada", - "ToastLibraryScanFailedToStart": "Error al iniciar la exploración", + "ToastLibraryScanFailedToStart": "Error al iniciar el escaneo", "ToastLibraryScanStarted": "Se inició el escaneo de la biblioteca", - "ToastLibraryUpdateFailed": "Error al actualizar biblioteca", + "ToastLibraryUpdateFailed": "Error al actualizar la biblioteca", "ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualizada", "ToastPlaylistCreateFailed": "Error al crear la lista de reproducción.", "ToastPlaylistCreateSuccess": "Lista de reproducción creada", @@ -697,15 +697,15 @@ "ToastPlaylistUpdateFailed": "Error al actualizar la lista de reproducción.", "ToastPlaylistUpdateSuccess": "Lista de reproducción actualizada", "ToastPodcastCreateFailed": "Error al crear podcast", - "ToastPodcastCreateSuccess": "Podcast creada", + "ToastPodcastCreateSuccess": "Podcast creado", "ToastRemoveItemFromCollectionFailed": "Error al eliminar el elemento de la colección", "ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección.", "ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS", "ToastRSSFeedCloseSuccess": "Fuente RSS cerrada", - "ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device", - "ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"", + "ToastSendEbookToDeviceFailed": "Error al enviar el ebook al dispositivo", + "ToastSendEbookToDeviceSuccess": "Ebook enviado al dispositivo \"{0}\"", "ToastSeriesUpdateFailed": "Error al actualizar la serie", - "ToastSeriesUpdateSuccess": "Series actualizada", + "ToastSeriesUpdateSuccess": "Serie actualizada", "ToastSessionDeleteFailed": "Error al eliminar sesión", "ToastSessionDeleteSuccess": "Sesión eliminada", "ToastSocketConnected": "Socket conectado", @@ -713,4 +713,4 @@ "ToastSocketFailedToConnect": "Error al conectar al Socket", "ToastUserDeleteFailed": "Error al eliminar el usuario", "ToastUserDeleteSuccess": "Usuario eliminado" -} \ No newline at end of file +} From 5d7c197c893d10277f59c753e2d324837185a78f Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 3 Oct 2023 19:43:37 +0000 Subject: [PATCH 030/208] [fix] Add back toLowerCase to cleanAuthor/Title (required by other uses) --- server/finders/BookFinder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index b30510f22..aa66fb920 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -59,12 +59,12 @@ class BookFinder { // Remove single quotes (i.e. "Ender's Game" becomes "Enders Game") cleaned = cleaned.replace(/'/g, '') - return this.replaceAccentedChars(cleaned) + return this.replaceAccentedChars(cleaned).toLowerCase() } cleanAuthorForCompares(author) { if (!author) return '' - return this.replaceAccentedChars(author) + return this.replaceAccentedChars(author).toLowerCase() } filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) { From 401bd912043c4a04bfb5b4240aabdde26e84cd3c Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 3 Oct 2023 17:16:49 -0500 Subject: [PATCH 031/208] Add:Show current book duration on match page as compared with book listed #1803 --- client/components/cards/BookMatchCard.vue | 30 ++++++++++++++------ client/components/modals/item/tabs/Match.vue | 12 +++++--- client/plugins/utils.js | 9 ++++-- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/client/components/cards/BookMatchCard.vue b/client/components/cards/BookMatchCard.vue index dd782d30d..77619e550 100644 --- a/client/components/cards/BookMatchCard.vue +++ b/client/components/cards/BookMatchCard.vue @@ -15,8 +15,8 @@

by {{ book.author }}

Narrated by {{ book.narrator }}

-

Runtime: {{ $elapsedPrettyExtended(book.duration * 60) }}

-
+

Runtime: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}

+

{{ series.series }} #{{ series.sequence }} @@ -29,9 +29,7 @@

-
- {{ book.title }} -
+
{{ book.title }}

by {{ book.author }}

{{ book.genres.join(', ') }}

@@ -56,7 +54,8 @@ export default { default: () => {} }, isPodcast: Boolean, - bookCoverAspectRatio: Number + bookCoverAspectRatio: Number, + currentBookDuration: Number }, data() { return { @@ -65,12 +64,27 @@ export default { }, computed: { bookCovers() { - return this.book.covers ? this.book.covers || [] : [] + return this.book.covers || [] + }, + bookDuration() { + return (this.book.duration || 0) * 60 + }, + bookDurationComparison() { + if (!this.bookDuration || !this.currentBookDuration) return '' + let differenceInSeconds = this.currentBookDuration - this.bookDuration + // Only show seconds on difference if difference is less than an hour + if (differenceInSeconds < 0) { + differenceInSeconds = Math.abs(differenceInSeconds) + return `(${this.$elapsedPrettyExtended(differenceInSeconds, false, differenceInSeconds < 3600)} shorter)` + } else if (differenceInSeconds > 0) { + return `(${this.$elapsedPrettyExtended(differenceInSeconds, false, differenceInSeconds < 3600)} longer)` + } + return '(exact match)' } }, methods: { selectMatch() { - var book = { ...this.book } + const book = { ...this.book } book.cover = this.selectedCover this.$emit('select', book) }, diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 0c5d67eba..1c682919f 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -22,7 +22,7 @@
@@ -205,7 +205,7 @@ export default { processing: Boolean, libraryItem: { type: Object, - default: () => { } + default: () => {} } }, data() { @@ -290,13 +290,17 @@ export default { return this.$strings.LabelSearchTitle }, media() { - return this.libraryItem ? this.libraryItem.media || {} : {} + return this.libraryItem?.media || {} }, mediaMetadata() { return this.media.metadata || {} }, + currentBookDuration() { + if (this.isPodcast) return 0 + return this.media.duration || 0 + }, mediaType() { - return this.libraryItem ? this.libraryItem.mediaType : null + return this.libraryItem?.mediaType || null }, isPodcast() { return this.mediaType == 'podcast' diff --git a/client/plugins/utils.js b/client/plugins/utils.js index 439f65c5d..495a14ef0 100644 --- a/client/plugins/utils.js +++ b/client/plugins/utils.js @@ -54,7 +54,7 @@ Vue.prototype.$secondsToTimestamp = (seconds, includeMs = false, alwaysIncludeHo return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}${msString}` } -Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => { +Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = true) => { if (isNaN(seconds) || seconds === null) return '' seconds = Math.round(seconds) @@ -69,11 +69,16 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => { hours -= days * 24 } + // If not showing seconds then round minutes up + if (minutes && seconds && !showSeconds) { + if (seconds >= 30) minutes++ + } + const strs = [] if (days) strs.push(`${days}d`) if (hours) strs.push(`${hours}h`) if (minutes) strs.push(`${minutes}m`) - if (seconds) strs.push(`${seconds}s`) + if (seconds && showSeconds) strs.push(`${seconds}s`) return strs.join(' ') } From 10f5bc8cbeeacd3c47f7115f387dd7d5817982e7 Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 4 Oct 2023 05:26:16 +0000 Subject: [PATCH 032/208] [cleanup] Make original title/author check with more readable --- server/finders/BookFinder.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index aa66fb920..6ca238ee6 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -276,7 +276,6 @@ class BookFinder { // Now run up to maxFuzzySearches fuzzy searches let titleCandidates = new BookFinder.TitleCandidates(this, cleanAuthor) - titleCandidates.add(title) // remove parentheses and their contents, and replace with a separator const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}/g, " - ") @@ -285,16 +284,15 @@ class BookFinder { for (const titlePart of titleParts) { titleCandidates.add(titlePart) } - // We already searched for original title - if (author == cleanAuthor) titleCandidates.delete(title) if (titleCandidates.size > 0) { titleCandidates = titleCandidates.getCandidates() for (const titleCandidate of titleCandidates) { + if (titleCandidate == title && cleanAuthor == author) continue // We already tried this if (++numFuzzySearches > maxFuzzySearches) return books books = await this.runSearch(titleCandidate, cleanAuthor, provider, asin, maxTitleDistance, maxAuthorDistance) if (books.length) break } - if (!books.length) { + if (!books.length && cleanAuthor) { // Now try searching without the author for (const titleCandidate of titleCandidates) { if (++numFuzzySearches > maxFuzzySearches) return books From 752bfffb1109e8fadf87775ecacf588365608b03 Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 4 Oct 2023 14:53:12 +0000 Subject: [PATCH 033/208] [enhamcement] Only add title candidate before and after all transforms --- server/finders/BookFinder.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 6ca238ee6..1fe86718e 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -206,12 +206,11 @@ class BookFinder { let candidate = cleanTitle - for (const transformer of titleTransformers) { + for (const transformer of titleTransformers) candidate = candidate.replace(transformer[0], transformer[1]).trim() - if (candidate) { - this.candidates.add(candidate) - } - } + + if (candidate) + this.candidates.add(candidate) } get size() { From bfe514b7d4683a7b8b4608a58d298ec591b88732 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 4 Oct 2023 17:05:12 -0500 Subject: [PATCH 034/208] Add:Email inputs for users --- client/components/modals/AccountModal.vue | 14 +++++++++----- client/components/tables/UsersTable.vue | 1 - server/controllers/UserController.js | 8 ++++++++ server/models/User.js | 2 ++ server/objects/user/User.js | 6 +++++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/client/components/modals/AccountModal.vue b/client/components/modals/AccountModal.vue index a09de35dd..ddad3cd38 100644 --- a/client/components/modals/AccountModal.vue +++ b/client/components/modals/AccountModal.vue @@ -14,13 +14,17 @@
+
-
- +
+
-
+
+ +
+

{{ $strings.LabelEnable }}

@@ -257,7 +261,6 @@ export default { if (account.type === 'root' && !account.isActive) return this.processing = true - console.log('Calling update', account) this.$axios .$patch(`/api/users/${this.account.id}`, account) .then((data) => { @@ -329,6 +332,7 @@ export default { if (this.account) { this.newUser = { username: this.account.username, + email: this.account.email, password: this.account.password, type: this.account.type, isActive: this.account.isActive, @@ -337,9 +341,9 @@ export default { itemTagsSelected: [...(this.account.itemTagsSelected || [])] } } else { - this.fetchAllTags() this.newUser = { username: null, + email: null, password: null, type: 'user', isActive: true, diff --git a/client/components/tables/UsersTable.vue b/client/components/tables/UsersTable.vue index cfcf3f47c..863012b5d 100644 --- a/client/components/tables/UsersTable.vue +++ b/client/components/tables/UsersTable.vue @@ -129,7 +129,6 @@ export default { this.users = res.users.sort((a, b) => { return a.createdAt - b.createdAt }) - console.log('Loaded users', this.users) }) .catch((error) => { console.error('Failed', error) diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index a3f70e20d..2695a7a0f 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -115,6 +115,13 @@ class UserController { } } + /** + * PATCH: /api/users/:id + * Update user + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ async update(req, res) { const user = req.reqUser @@ -126,6 +133,7 @@ class UserController { var account = req.body var shouldUpdateToken = false + // When changing username create a new API token if (account.username !== undefined && account.username !== user.username) { const usernameExists = await Database.userModel.getUserByUsername(account.username) if (usernameExists) { diff --git a/server/models/User.js b/server/models/User.js index 6f457aa5b..bf22a3a58 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -59,6 +59,7 @@ class User extends Model { id: userExpanded.id, oldUserId: userExpanded.extraData?.oldUserId || null, username: userExpanded.username, + email: userExpanded.email || null, pash: userExpanded.pash, type: userExpanded.type, token: userExpanded.token, @@ -96,6 +97,7 @@ class User extends Model { return { id: oldUser.id, username: oldUser.username, + email: oldUser.email || null, pash: oldUser.pash || null, type: oldUser.type || null, token: oldUser.token || null, diff --git a/server/objects/user/User.js b/server/objects/user/User.js index 1ed74bb20..a9c9c767b 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -7,6 +7,7 @@ class User { this.id = null this.oldUserId = null // TODO: Temp for keeping old access tokens this.username = null + this.email = null this.pash = null this.type = null this.token = null @@ -76,6 +77,7 @@ class User { id: this.id, oldUserId: this.oldUserId, username: this.username, + email: this.email, pash: this.pash, type: this.type, token: this.token, @@ -97,6 +99,7 @@ class User { id: this.id, oldUserId: this.oldUserId, username: this.username, + email: this.email, type: this.type, token: (this.type === 'root' && hideRootToken) ? '' : this.token, mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [], @@ -140,6 +143,7 @@ class User { this.id = user.id this.oldUserId = user.oldUserId this.username = user.username + this.email = user.email || null this.pash = user.pash this.type = user.type this.token = user.token @@ -184,7 +188,7 @@ class User { update(payload) { var hasUpdates = false // Update the following keys: - const keysToCheck = ['pash', 'type', 'username', 'isActive'] + const keysToCheck = ['pash', 'type', 'username', 'email', 'isActive'] keysToCheck.forEach((key) => { if (payload[key] !== undefined) { if (key === 'isActive' || payload[key]) { // pash, type, username must evaluate to true (cannot be null or empty) From 8979586404a1ca4a46b0eff3d1cc23582ffbfbb5 Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 5 Oct 2023 10:28:55 +0000 Subject: [PATCH 035/208] [enhancement] Improve candidate sorting --- server/finders/BookFinder.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 1fe86718e..2bd1c5716 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -189,9 +189,11 @@ class BookFinder { this.bookFinder = bookFinder this.candidates = new Set() this.cleanAuthor = cleanAuthor + this.priorities = {} + this.positions = {} } - add(title) { + add(title, position = 0) { const titleTransformers = [ [/([,:;_]| by ).*/g, ''], // Remove subtitle [/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers @@ -203,14 +205,22 @@ class BookFinder { const cleanTitle = this.bookFinder.cleanTitleForCompares(title).trim() if (!cleanTitle) return this.candidates.add(cleanTitle) + this.priorities[cleanTitle] = 0 + this.positions[cleanTitle] = position let candidate = cleanTitle for (const transformer of titleTransformers) candidate = candidate.replace(transformer[0], transformer[1]).trim() - if (candidate) - this.candidates.add(candidate) + if (candidate != cleanTitle) { + if (candidate) { + this.candidates.add(candidate) + this.priorities[candidate] = 0 + this.positions[candidate] = position + } + this.priorities[cleanTitle] = 1 + } } get size() { @@ -227,6 +237,12 @@ class BookFinder { const onlyDigits = /^\d+$/ const includesOnlyDigitsDiff = !onlyDigits.test(b) - !onlyDigits.test(a) if (includesOnlyDigitsDiff) return includesOnlyDigitsDiff + // transformed candidates receive higher priority + const priorityDiff = this.priorities[a] - this.priorities[b] + if (priorityDiff) return priorityDiff + // if same priorirty, prefer candidates that are closer to the beginning (e.g. titles before subtitles) + const positionDiff = this.positions[a] - this.positions[b] + if (positionDiff) return positionDiff // Start with longer candidaets, as they are likely more specific const lengthDiff = b.length - a.length if (lengthDiff) return lengthDiff @@ -280,8 +296,8 @@ class BookFinder { const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}/g, " - ") // Split title into hypen-separated parts const titleParts = cleanTitle.split(/ - | -|- /) - for (const titlePart of titleParts) { - titleCandidates.add(titlePart) + for (const [position, titlePart] of titleParts.entries()) { + titleCandidates.add(titlePart, position) } if (titleCandidates.size > 0) { titleCandidates = titleCandidates.getCandidates() From 9eff471afaa87572bfcb312af64d756511fde2a3 Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 5 Oct 2023 11:39:29 +0000 Subject: [PATCH 036/208] [enhancement] AuthorCandidates, author validation --- server/finders/BookFinder.js | 100 +++++++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 16 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 2bd1c5716..b29417cb3 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -194,6 +194,12 @@ class BookFinder { } add(title, position = 0) { + // if title contains the author, remove it + if (this.cleanAuthor) { + const authorRe = new RegExp(`(^| | by |)${this.cleanAuthor}(?= |$)`, "g") + title = this.bookFinder.cleanAuthorForCompares(title).replace(authorRe, '').trim() + } + const titleTransformers = [ [/([,:;_]| by ).*/g, ''], // Remove subtitle [/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers @@ -258,6 +264,73 @@ class BookFinder { } } + static AuthorCandidates = class { + constructor(bookFinder, cleanAuthor) { + this.bookFinder = bookFinder + this.candidates = new Set() + this.cleanAuthor = cleanAuthor + if (cleanAuthor) this.candidates.add(cleanAuthor) + } + + validateAuthor(name, region = '', maxLevenshtein = 3) { + return this.bookFinder.audnexus.authorASINsRequest(name, region).then((asins) => { + for (const asin of asins) { + let cleanName = this.bookFinder.cleanAuthorForCompares(asin.name) + if (!cleanName) continue + if (cleanName.includes(name)) return name + if (name.includes(cleanName)) return cleanName + if (levenshteinDistance(cleanName, name) <= maxLevenshtein) return cleanName + } + return '' + }) + } + + add(author) { + const authorTransformers = [] + + // Main variant + const cleanAuthor = this.bookFinder.cleanAuthorForCompares(author).trim() + if (!cleanAuthor) return false + this.candidates.add(cleanAuthor) + + let candidate = cleanAuthor + + for (const transformer of authorTransformers) { + candidate = candidate.replace(transformer[0], transformer[1]).trim() + if (candidate) { + this.candidates.add(candidate) + } + } + + return true + } + + get size() { + return this.candidates.size + } + + async getCandidates() { + var filteredCandidates = [] + var promises = [] + for (const candidate of this.candidates) { + promises.push(this.validateAuthor(candidate)) + } + const results = [...new Set(await Promise.all(promises))] + filteredCandidates = results.filter(author => author) + // if no valid candidates were found, add back the original clean author + if (!filteredCandidates.length && this.cleanAuthor) filteredCandidates.push(this.cleanAuthor) + // always add an empty author candidate + filteredCandidates.push('') + Logger.debug(`[${this.constructor.name}] Found ${filteredCandidates.length} fuzzy author candidates`) + Logger.debug(filteredCandidates) + return filteredCandidates + } + + delete(author) { + return this.candidates.delete(author) + } + } + /** * Search for books including fuzzy searches @@ -290,30 +363,25 @@ class BookFinder { const cleanAuthor = this.cleanAuthorForCompares(author) // Now run up to maxFuzzySearches fuzzy searches - let titleCandidates = new BookFinder.TitleCandidates(this, cleanAuthor) + let authorCandidates = new BookFinder.AuthorCandidates(this, cleanAuthor) // remove parentheses and their contents, and replace with a separator const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}/g, " - ") // Split title into hypen-separated parts const titleParts = cleanTitle.split(/ - | -|- /) - for (const [position, titlePart] of titleParts.entries()) { - titleCandidates.add(titlePart, position) - } - if (titleCandidates.size > 0) { + for (const titlePart of titleParts) + authorCandidates.add(titlePart) + authorCandidates = await authorCandidates.getCandidates() + for (const authorCandidate of authorCandidates) { + let titleCandidates = new BookFinder.TitleCandidates(this, authorCandidate) + for (const [position, titlePart] of titleParts.entries()) + titleCandidates.add(titlePart, position) titleCandidates = titleCandidates.getCandidates() for (const titleCandidate of titleCandidates) { - if (titleCandidate == title && cleanAuthor == author) continue // We already tried this + if (titleCandidate == title && authorCandidate == author) continue // We already tried this if (++numFuzzySearches > maxFuzzySearches) return books - books = await this.runSearch(titleCandidate, cleanAuthor, provider, asin, maxTitleDistance, maxAuthorDistance) - if (books.length) break - } - if (!books.length && cleanAuthor) { - // Now try searching without the author - for (const titleCandidate of titleCandidates) { - if (++numFuzzySearches > maxFuzzySearches) return books - books = await this.runSearch(titleCandidate, '', provider, asin, maxTitleDistance, maxAuthorDistance) - if (books.length) break - } + books = await this.runSearch(titleCandidate, authorCandidate, provider, asin, maxTitleDistance, maxAuthorDistance) + if (books.length) return books } } } From b2acdadcea6fa52636d816166beac24cb370e127 Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 5 Oct 2023 12:22:02 +0000 Subject: [PATCH 037/208] [enhancement] Added a couple title transformers --- server/finders/BookFinder.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index b29417cb3..8876e2bd0 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -202,9 +202,11 @@ class BookFinder { const titleTransformers = [ [/([,:;_]| by ).*/g, ''], // Remove subtitle - [/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers [/(^| )\d+k(bps)?( |$)/, ' '], // Remove bitrate - [/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''] // Remove edition + [/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''], // Remove edition + [/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type + [/ a novel.*$/g, ''], // Remove "a novel" + [/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers ] // Main variant From f3555a12ceff25d328b7dd1637668874e181946e Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 5 Oct 2023 14:50:16 +0000 Subject: [PATCH 038/208] [enhancement] Handle initials in author normalization --- server/finders/BookFinder.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 8876e2bd0..70031fa38 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -64,7 +64,12 @@ class BookFinder { cleanAuthorForCompares(author) { if (!author) return '' - return this.replaceAccentedChars(author).toLowerCase() + let cleanAuthor = this.replaceAccentedChars(author).toLowerCase() + // separate initials + cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2') + // remove middle initials + cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '') + return cleanAuthor } filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) { From bf9f3895db17f2172cda4e32caab559eda9c05a1 Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 5 Oct 2023 17:53:54 +0000 Subject: [PATCH 039/208] [enhancement] Treat underscores as title part separators --- server/finders/BookFinder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 70031fa38..e3e87f4a2 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -372,8 +372,8 @@ class BookFinder { // Now run up to maxFuzzySearches fuzzy searches let authorCandidates = new BookFinder.AuthorCandidates(this, cleanAuthor) - // remove parentheses and their contents, and replace with a separator - const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}/g, " - ") + // remove underscores and parentheses with their contents, and replace with a separator + const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, " - ") // Split title into hypen-separated parts const titleParts = cleanTitle.split(/ - | -|- /) for (const titlePart of titleParts) From b0b7a0a61817671b15e2687a32399aea6f0bdb51 Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 5 Oct 2023 18:27:52 +0000 Subject: [PATCH 040/208] [enhancement] Reduce spurious matches in validateAuthor --- server/finders/BookFinder.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index e3e87f4a2..d31921425 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -279,9 +279,10 @@ class BookFinder { if (cleanAuthor) this.candidates.add(cleanAuthor) } - validateAuthor(name, region = '', maxLevenshtein = 3) { + validateAuthor(name, region = '', maxLevenshtein = 2) { return this.bookFinder.audnexus.authorASINsRequest(name, region).then((asins) => { - for (const asin of asins) { + for (const [i, asin] of asins.entries()) { + if (i > 10) break let cleanName = this.bookFinder.cleanAuthorForCompares(asin.name) if (!cleanName) continue if (cleanName.includes(name)) return name From f44b7ed1d0f8ba538e194632f98660893d9206a6 Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 5 Oct 2023 18:41:18 +0000 Subject: [PATCH 041/208] [enhancement] If no valid authors, use clean author field --- server/finders/BookFinder.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index d31921425..8c420333c 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -317,6 +317,14 @@ class BookFinder { return this.candidates.size } + get agressivelyCleanAuthor() { + if (this.cleanAuthor) { + const agressivelyCleanAuthor = this.cleanAuthor.replace(/[,/-].*$/, '').trim() + return agressivelyCleanAuthor ? agressivelyCleanAuthor : this.cleanAuthor + } + return '' + } + async getCandidates() { var filteredCandidates = [] var promises = [] @@ -325,9 +333,9 @@ class BookFinder { } const results = [...new Set(await Promise.all(promises))] filteredCandidates = results.filter(author => author) - // if no valid candidates were found, add back the original clean author - if (!filteredCandidates.length && this.cleanAuthor) filteredCandidates.push(this.cleanAuthor) - // always add an empty author candidate + // If no valid candidates were found, add back an aggresively cleaned author version + if (!filteredCandidates.length && this.cleanAuthor) filteredCandidates.push(this.agressivelyCleanAuthor) + // Always add an empty author candidate filteredCandidates.push('') Logger.debug(`[${this.constructor.name}] Found ${filteredCandidates.length} fuzzy author candidates`) Logger.debug(filteredCandidates) @@ -364,7 +372,7 @@ class BookFinder { books = await this.runSearch(title, author, provider, asin, maxTitleDistance, maxAuthorDistance) if (!books.length && maxFuzzySearches > 0) { - // normalize title and author + // Normalize title and author title = title.trim().toLowerCase() author = author.trim().toLowerCase() @@ -373,7 +381,7 @@ class BookFinder { // Now run up to maxFuzzySearches fuzzy searches let authorCandidates = new BookFinder.AuthorCandidates(this, cleanAuthor) - // remove underscores and parentheses with their contents, and replace with a separator + // Remove underscores and parentheses with their contents, and replace with a separator const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, " - ") // Split title into hypen-separated parts const titleParts = cleanTitle.split(/ - | -|- /) From b447cf5c1ccb273fd45af8a7e8b11f3844c28fe6 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 5 Oct 2023 17:00:40 -0500 Subject: [PATCH 042/208] Fix:Handle non-ascii characters in global search by not lowercasing in query #2187 --- server/controllers/LibraryController.js | 5 ++-- server/utils/index.js | 23 +++++++++++++++++++ .../utils/queries/libraryItemsBookFilters.js | 4 +++- .../queries/libraryItemsPodcastFilters.js | 4 +++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index f768bb935..b25e02aaf 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -9,7 +9,8 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter const libraryItemFilters = require('../utils/queries/libraryItemFilters') const seriesFilters = require('../utils/queries/seriesFilters') const fileUtils = require('../utils/fileUtils') -const { sort, createNewSortInstance } = require('../libs/fastSort') +const { asciiOnlyToLowerCase } = require('../utils/index') +const { createNewSortInstance } = require('../libs/fastSort') const naturalSort = createNewSortInstance({ comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare }) @@ -555,7 +556,7 @@ class LibraryController { return res.status(400).send('No query string') } const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12 - const query = req.query.q.trim().toLowerCase() + const query = asciiOnlyToLowerCase(req.query.q.trim()) const matches = await libraryItemFilters.search(req.user, req.library, query, limit) res.json(matches) diff --git a/server/utils/index.js b/server/utils/index.js index 5797b0b51..abcc626c0 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -166,4 +166,27 @@ module.exports.getTitleIgnorePrefix = (title) => { module.exports.getTitlePrefixAtEnd = (title) => { let [sort, prefix] = getTitleParts(title) return prefix ? `${sort}, ${prefix}` : title +} + +/** + * to lower case for only ascii characters + * used to handle sqlite that doesnt support unicode lower + * @see https://github.com/advplyr/audiobookshelf/issues/2187 + * + * @param {string} str + * @returns {string} + */ +module.exports.asciiOnlyToLowerCase = (str) => { + if (!str) return '' + + let temp = '' + for (let chars of str) { + let value = chars.charCodeAt() + if (value >= 65 && value <= 90) { + temp += String.fromCharCode(value + 32) + } else { + temp += chars + } + } + return temp } \ No newline at end of file diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 10e1101da..d23459b46 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -2,6 +2,7 @@ const Sequelize = require('sequelize') const Database = require('../../Database') const Logger = require('../../Logger') const authorFilters = require('./authorFilters') +const { asciiOnlyToLowerCase } = require('../index') module.exports = { /** @@ -1013,7 +1014,8 @@ module.exports = { let matchText = null let matchKey = null for (const key of ['title', 'subtitle', 'asin', 'isbn']) { - if (book[key]?.toLowerCase().includes(query)) { + const valueToLower = asciiOnlyToLowerCase(book[key]) + if (valueToLower.includes(query)) { matchText = book[key] matchKey = key break diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 27ac3fcd8..7665c89ba 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -2,6 +2,7 @@ const Sequelize = require('sequelize') const Database = require('../../Database') const Logger = require('../../Logger') +const { asciiOnlyToLowerCase } = require('../index') module.exports = { /** @@ -364,7 +365,8 @@ module.exports = { let matchText = null let matchKey = null for (const key of ['title', 'author', 'itunesId', 'itunesArtistId']) { - if (podcast[key]?.toLowerCase().includes(query)) { + const valueToLower = asciiOnlyToLowerCase(podcast[key]) + if (valueToLower.includes(query)) { matchText = podcast[key] matchKey = key break From db9d5c9d4329ce7ac3614dfb1d5cd4aed15eac0a Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 6 Oct 2023 16:52:12 -0500 Subject: [PATCH 043/208] Add:Support for pasting semicolon separated strings in multi select inputs #1198 --- client/components/ui/MultiSelect.vue | 27 +++++++++++++++- .../components/ui/MultiSelectQueryInput.vue | 31 ++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue index f2c542eb9..4fa8e394f 100644 --- a/client/components/ui/MultiSelect.vue +++ b/client/components/ui/MultiSelect.vue @@ -11,7 +11,7 @@
{{ item }}
- +
@@ -145,6 +145,31 @@ export default { this.menu.style.left = boundingBox.x + 'px' this.menu.style.width = boundingBox.width + 'px' }, + inputPaste(evt) { + setTimeout(() => { + const pastedText = evt.target?.value || '' + console.log('Pasted text=', pastedText) + const pastedItems = [ + ...new Set( + pastedText + .split(';') + .map((i) => i.trim()) + .filter((i) => i) + ) + ] + + // Filter out items already selected + const itemsToAdd = pastedItems.filter((i) => !this.selected.some((_i) => _i.toLowerCase() === i.toLowerCase())) + if (pastedItems.length && !itemsToAdd.length) { + this.textInput = null + this.currentSearch = null + } else { + for (const itemToAdd of itemsToAdd) { + this.insertNewItem(itemToAdd) + } + } + }, 10) + }, inputFocus() { if (!this.menu) { this.unmountMountMenu() diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue index fb9528cea..c86d32289 100644 --- a/client/components/ui/MultiSelectQueryInput.vue +++ b/client/components/ui/MultiSelectQueryInput.vue @@ -14,7 +14,7 @@
add
- +
@@ -112,6 +112,7 @@ export default { return !!this.selected.find((i) => i.id === itemValue) }, search() { + if (!this.textInput) return this.currentSearch = this.textInput const dataToSearch = this.filterData[this.filterKey] || [] @@ -165,6 +166,34 @@ export default { this.menu.style.left = boundingBox.x + 'px' this.menu.style.width = boundingBox.width + 'px' }, + inputPaste(evt) { + setTimeout(() => { + const pastedText = evt.target?.value || '' + console.log('Pasted text=', pastedText) + const pastedItems = [ + ...new Set( + pastedText + .split(';') + .map((i) => i.trim()) + .filter((i) => i) + ) + ] + + // Filter out items already selected + const itemsToAdd = pastedItems.filter((i) => !this.selected.some((_i) => _i[this.textKey].toLowerCase() === i.toLowerCase())) + if (pastedItems.length && !itemsToAdd.length) { + this.textInput = null + this.currentSearch = null + } else { + for (const [index, itemToAdd] of itemsToAdd.entries()) { + this.insertNewItem({ + id: `new-${Date.now()}-${index}`, + name: itemToAdd + }) + } + } + }, 10) + }, inputFocus() { if (!this.menu) { this.unmountMountMenu() From f8f555b4b6ce1ef64dea6913a42d37fabc0f105f Mon Sep 17 00:00:00 2001 From: mikiher Date: Sat, 7 Oct 2023 21:28:25 +0000 Subject: [PATCH 044/208] Remove some unused code in AuthorCandidates.add --- server/finders/BookFinder.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 54ac63a4b..a0b64f552 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -294,23 +294,9 @@ class BookFinder { } add(author) { - const authorTransformers = [] - - // Main variant const cleanAuthor = this.bookFinder.cleanAuthorForCompares(author).trim() - if (!cleanAuthor) return false + if (!cleanAuthor) return this.candidates.add(cleanAuthor) - - let candidate = cleanAuthor - - for (const transformer of authorTransformers) { - candidate = candidate.replace(transformer[0], transformer[1]).trim() - if (candidate) { - this.candidates.add(candidate) - } - } - - return true } get size() { From 347b49f5645619f53144e92e2dec961f1d025b32 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 8 Oct 2023 17:10:43 -0500 Subject: [PATCH 045/208] Update:Remove scanner settings, add library scanner settings tab, add order of precedence --- .../components/modals/libraries/EditModal.vue | 16 +- .../libraries/LibraryScannerSettings.vue | 129 ++++++ .../components/tables/library/LibraryItem.vue | 13 +- client/pages/config/index.vue | 130 +++--- client/strings/de.json | 6 - client/strings/en-us.json | 8 +- client/strings/es.json | 8 +- client/strings/fr.json | 6 - client/strings/gu.json | 6 - client/strings/hi.json | 6 - client/strings/hr.json | 6 - client/strings/it.json | 6 - client/strings/lt.json | 6 - client/strings/nl.json | 8 +- client/strings/no.json | 6 - client/strings/pl.json | 6 - client/strings/ru.json | 6 - client/strings/zh-cn.json | 6 - server/controllers/LibraryController.js | 12 +- server/models/Library.js | 1 + server/objects/mediaTypes/Book.js | 211 ---------- server/objects/mediaTypes/Music.js | 4 - server/objects/mediaTypes/Podcast.js | 31 -- server/objects/settings/LibrarySettings.js | 17 +- server/objects/settings/ServerSettings.js | 9 - server/scanner/AbsMetadataFileScanner.js | 65 +++ server/scanner/AudioFileScanner.js | 202 ++++++++++ server/scanner/BookScanner.js | 379 ++++-------------- server/scanner/LibraryItemScanData.js | 36 +- server/scanner/LibraryItemScanner.js | 13 +- server/scanner/LibraryScanner.js | 33 +- server/scanner/OpfFileScanner.js | 48 +++ server/scanner/PodcastScanner.js | 19 +- .../parsers/parseOverdriveMediaMarkers.js | 33 +- server/utils/scandir.js | 82 ++-- 35 files changed, 764 insertions(+), 809 deletions(-) create mode 100644 client/components/modals/libraries/LibraryScannerSettings.vue create mode 100644 server/scanner/AbsMetadataFileScanner.js create mode 100644 server/scanner/OpfFileScanner.js diff --git a/client/components/modals/libraries/EditModal.vue b/client/components/modals/libraries/EditModal.vue index 633b76465..1fd011cf7 100644 --- a/client/components/modals/libraries/EditModal.vue +++ b/client/components/modals/libraries/EditModal.vue @@ -54,6 +54,9 @@ export default { buttonText() { return this.library ? this.$strings.ButtonSave : this.$strings.ButtonCreate }, + mediaType() { + return this.libraryCopy?.mediaType + }, tabs() { return [ { @@ -66,12 +69,19 @@ export default { title: this.$strings.HeaderSettings, component: 'modals-libraries-library-settings' }, + { + id: 'scanner', + title: this.$strings.HeaderSettingsScanner, + component: 'modals-libraries-library-scanner-settings' + }, { id: 'schedule', title: this.$strings.HeaderSchedule, component: 'modals-libraries-schedule-scan' } - ] + ].filter((tab) => { + return tab.id !== 'scanner' || this.mediaType === 'book' + }) }, tabName() { var _tab = this.tabs.find((t) => t.id === this.selectedTab) @@ -105,7 +115,9 @@ export default { disableWatcher: false, skipMatchingMediaWithAsin: false, skipMatchingMediaWithIsbn: false, - autoScanCronExpression: null + autoScanCronExpression: null, + hideSingleBookSeries: false, + metadataPrecedence: ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] } } }, diff --git a/client/components/modals/libraries/LibraryScannerSettings.vue b/client/components/modals/libraries/LibraryScannerSettings.vue new file mode 100644 index 000000000..95ae801a7 --- /dev/null +++ b/client/components/modals/libraries/LibraryScannerSettings.vue @@ -0,0 +1,129 @@ + + + \ No newline at end of file diff --git a/client/components/tables/library/LibraryItem.vue b/client/components/tables/library/LibraryItem.vue index b84dec446..cfb30a0c3 100644 --- a/client/components/tables/library/LibraryItem.vue +++ b/client/components/tables/library/LibraryItem.vue @@ -74,6 +74,11 @@ export default { } ] if (this.isBookLibrary) { + items.push({ + text: this.$strings.ButtonForceReScan, + action: 'force-rescan', + value: 'force-rescan' + }) items.push({ text: this.$strings.ButtonMatchBooks, action: 'match-books', @@ -95,8 +100,8 @@ export default { this.editClick() } else if (action === 'scan') { this.scan() - } else if (action === 'force-scan') { - this.forceScan() + } else if (action === 'force-rescan') { + this.scan(true) } else if (action === 'match-books') { this.matchAll() } else if (action === 'delete') { @@ -121,9 +126,9 @@ export default { editClick() { this.$emit('edit', this.library) }, - scan() { + scan(force = false) { this.$store - .dispatch('libraries/requestLibraryScan', { libraryId: this.library.id }) + .dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force }) .then(() => { this.$toast.success(this.$strings.ToastLibraryScanStarted) }) diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 673911415..936f6a303 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -51,6 +51,56 @@
+
+

{{ $strings.HeaderSettingsScanner }}

+
+ +
+ + +

+ {{ $strings.LabelSettingsParseSubtitles }} + info_outlined +

+
+
+ +
+ + +

+ {{ $strings.LabelSettingsFindCovers }} + info_outlined +

+
+
+
+
+ +
+ +
+ + +

+ {{ $strings.LabelSettingsPreferMatchedMetadata }} + info_outlined +

+
+
+ +
+ + +

+ {{ $strings.LabelSettingsEnableWatcher }} + info_outlined +

+
+
+
+ +

{{ $strings.HeaderSettingsDisplay }}

@@ -88,86 +138,6 @@
-
- -
-
-

{{ $strings.HeaderSettingsScanner }}

-
- -
- - -

- {{ $strings.LabelSettingsParseSubtitles }} - info_outlined -

-
-
- -
- - -

- {{ $strings.LabelSettingsFindCovers }} - info_outlined -

-
-
-
-
- -
- -
- - -

- {{ $strings.LabelSettingsOverdriveMediaMarkers }} - info_outlined -

-
-
- -
- - -

- {{ $strings.LabelSettingsPreferAudioMetadata }} - info_outlined -

-
-
- -
- - -

- {{ $strings.LabelSettingsPreferOPFMetadata }} - info_outlined -

-
-
- -
- - -

- {{ $strings.LabelSettingsPreferMatchedMetadata }} - info_outlined -

-
-
- -
- - -

- {{ $strings.LabelSettingsEnableWatcher }} - info_outlined -

-
-
-
+
delete @@ -16,15 +16,16 @@
-
+
upload
+
- - {{ $strings.ButtonSave }} + + {{ $strings.ButtonSubmit }}
@@ -64,7 +65,7 @@

{{ $strings.MessageNoCoversFound }}

@@ -165,6 +166,9 @@ export default { userCanUpload() { return this.$store.getters['user/getUserCanUpload'] }, + userCanDelete() { + return this.$store.getters['user/getUserCanDelete'] + }, userToken() { return this.$store.getters['user/getToken'] }, @@ -222,71 +226,53 @@ export default { this.coversFound = [] this.hasSearched = false } - this.imageUrl = this.media.coverPath || '' + this.imageUrl = '' this.searchTitle = this.mediaMetadata.title || '' this.searchAuthor = this.mediaMetadata.authorName || '' if (this.isPodcast) this.provider = 'itunes' else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google' }, removeCover() { - if (!this.media.coverPath) { - this.imageUrl = '' + if (!this.coverPath) { return } - this.updateCover('') + this.isProcessing = true + this.$axios + .$delete(`/api/items/${this.libraryItemId}/cover`) + .then(() => {}) + .catch((error) => { + console.error('Failed to remove cover', error) + if (error.response?.data) { + this.$toast.error(error.response.data) + } + }) + .finally(() => { + this.isProcessing = false + }) }, submitForm() { this.updateCover(this.imageUrl) }, async updateCover(cover) { - if (cover === this.coverPath) { - console.warn('Cover has not changed..', cover) + if (!cover.startsWith('http:') && !cover.startsWith('https:')) { + this.$toast.error('Invalid URL') return } this.isProcessing = true - var success = false - - if (!cover) { - // Remove cover - success = await this.$axios - .$delete(`/api/items/${this.libraryItemId}/cover`) - .then(() => true) - .catch((error) => { - console.error('Failed to remove cover', error) - if (error.response && error.response.data) { - this.$toast.error(error.response.data) - } - return false - }) - } else if (cover.startsWith('http:') || cover.startsWith('https:')) { - // Download cover from url and use - success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }).catch((error) => { - console.error('Failed to download cover from url', error) - if (error.response && error.response.data) { - this.$toast.error(error.response.data) - } - return false + this.$axios + .$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }) + .then(() => { + this.imageUrl = '' + this.$toast.success('Update Successful') }) - } else { - // Update local cover url - const updatePayload = { - cover - } - success = await this.$axios.$patch(`/api/items/${this.libraryItemId}/cover`, updatePayload).catch((error) => { - console.error('Failed to update', error) - if (error.response && error.response.data) { - this.$toast.error(error.response.data) - } - return false + .catch((error) => { + console.error('Failed to update cover', error) + this.$toast.error(error.response?.data || 'Failed to update cover') + }) + .finally(() => { + this.isProcessing = false }) - } - if (success) { - this.$toast.success('Update Successful') - } else if (this.media.coverPath) { - this.imageUrl = this.media.coverPath - } - this.isProcessing = false }, getSearchQuery() { var searchQuery = `provider=${this.provider}&title=${this.searchTitle}` @@ -319,7 +305,19 @@ export default { this.hasSearched = true }, setCover(coverFile) { - this.updateCover(coverFile.metadata.path) + this.isProcessing = true + this.$axios + .$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path }) + .then(() => { + this.$toast.success('Update Successful') + }) + .catch((error) => { + console.error('Failed to set local cover', error) + this.$toast.error(error.response?.data || 'Failed to set cover') + }) + .finally(() => { + this.isProcessing = false + }) } } } diff --git a/client/strings/de.json b/client/strings/de.json index ccd42ede6..b72df02f0 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Stunde", "LabelIcon": "Symbol", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "In die Titelliste aufnehmen", "LabelIncomplete": "Unvollständig", "LabelInProgress": "In Bearbeitung", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 37478bf0d..9195265ef 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Include in Tracklist", "LabelIncomplete": "Incomplete", "LabelInProgress": "In Progress", diff --git a/client/strings/es.json b/client/strings/es.json index f7d548ba0..f03c63528 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Hora", "LabelIcon": "Icono", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Incluir en Tracklist", "LabelIncomplete": "Incompleto", "LabelInProgress": "En Proceso", diff --git a/client/strings/fr.json b/client/strings/fr.json index 5d0e4b3ac..031462b28 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -266,6 +266,7 @@ "LabelHost": "Hôte", "LabelHour": "Heure", "LabelIcon": "Icone", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Inclure dans la liste des pistes", "LabelIncomplete": "Incomplet", "LabelInProgress": "En cours", diff --git a/client/strings/gu.json b/client/strings/gu.json index 5018cf4d3..0803ccf47 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Include in Tracklist", "LabelIncomplete": "Incomplete", "LabelInProgress": "In Progress", diff --git a/client/strings/hi.json b/client/strings/hi.json index 21ed9893e..1eea84954 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Include in Tracklist", "LabelIncomplete": "Incomplete", "LabelInProgress": "In Progress", diff --git a/client/strings/hr.json b/client/strings/hr.json index b0e0db911..47908b184 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Sat", "LabelIcon": "Ikona", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Dodaj u Tracklist", "LabelIncomplete": "Nepotpuno", "LabelInProgress": "U tijeku", diff --git a/client/strings/it.json b/client/strings/it.json index 96a243923..b60a87c1a 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Ora", "LabelIcon": "Icona", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Includi nella Tracklist", "LabelIncomplete": "Incompleta", "LabelInProgress": "In Corso", diff --git a/client/strings/lt.json b/client/strings/lt.json index 4f7bf2ed5..31d259e64 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -266,6 +266,7 @@ "LabelHost": "Serveris", "LabelHour": "Valanda", "LabelIcon": "Piktograma", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Įtraukti į takelių sąrašą", "LabelIncomplete": "Nebaigta", "LabelInProgress": "Vyksta", diff --git a/client/strings/nl.json b/client/strings/nl.json index ac61de96c..eb6b35b3a 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Uur", "LabelIcon": "Icoon", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Includeer in tracklijst", "LabelIncomplete": "Incompleet", "LabelInProgress": "Bezig", diff --git a/client/strings/no.json b/client/strings/no.json index d1f51aac8..f4fe316c8 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -266,6 +266,7 @@ "LabelHost": "Tjener", "LabelHour": "Time", "LabelIcon": "Ikon", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Inkluder i sporliste", "LabelIncomplete": "Ufullstendig", "LabelInProgress": "I gang", diff --git a/client/strings/pl.json b/client/strings/pl.json index c4e6ae848..a645877b8 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -266,6 +266,7 @@ "LabelHost": "Host", "LabelHour": "Godzina", "LabelIcon": "Ikona", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Dołącz do listy odtwarzania", "LabelIncomplete": "Nieukończone", "LabelInProgress": "W trakcie", diff --git a/client/strings/ru.json b/client/strings/ru.json index 3c95affa2..f7f569659 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -266,6 +266,7 @@ "LabelHost": "Хост", "LabelHour": "Часы", "LabelIcon": "Иконка", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "Включать в список воспроизведения", "LabelIncomplete": "Не завершен", "LabelInProgress": "В процессе", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 09eb6708b..1d7f90dd8 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -266,6 +266,7 @@ "LabelHost": "主机", "LabelHour": "小时", "LabelIcon": "图标", + "LabelImageURLFromTheWeb": "Image URL from the web", "LabelIncludeInTracklist": "包含在音轨列表中", "LabelIncomplete": "未听完", "LabelInProgress": "正在听", diff --git a/package-lock.json b/package-lock.json index 779480045..7178ac984 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", + "ssrf-req-filter": "^1.1.0", "xml2js": "^0.5.0" }, "bin": { @@ -2387,6 +2388,22 @@ } } }, + "node_modules/ssrf-req-filter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz", + "integrity": "sha512-YUyTinAEm52NsoDvkTFN9BQIa5nURNr2aN0BwOiJxHK3tlyGUczHa+2LjcibKNugAk/losB6kXOfcRzy0LQ4uA==", + "dependencies": { + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/ssrf-req-filter/node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "engines": { + "node": ">= 10" + } + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -4437,6 +4454,21 @@ "tar": "^6.1.11" } }, + "ssrf-req-filter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz", + "integrity": "sha512-YUyTinAEm52NsoDvkTFN9BQIa5nURNr2aN0BwOiJxHK3tlyGUczHa+2LjcibKNugAk/losB6kXOfcRzy0LQ4uA==", + "requires": { + "ipaddr.js": "^2.1.0" + }, + "dependencies": { + "ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==" + } + } + }, "ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -4672,4 +4704,4 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 4a00fa59c..e76147d86 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,10 @@ "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", + "ssrf-req-filter": "^1.1.0", "xml2js": "^0.5.0" }, "devDependencies": { "nodemon": "^2.0.20" } -} \ No newline at end of file +} diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index ac019a965..b0ecf446e 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -182,22 +182,22 @@ class LibraryItemController { return res.sendStatus(403) } - var libraryItem = req.libraryItem + let libraryItem = req.libraryItem - var result = null - if (req.body && req.body.url) { + let result = null + if (req.body?.url) { Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`) result = await CoverManager.downloadCoverFromUrl(libraryItem, req.body.url) - } else if (req.files && req.files.cover) { + } else if (req.files?.cover) { Logger.debug(`[LibraryItemController] Handling uploaded cover`) result = await CoverManager.uploadCover(libraryItem, req.files.cover) } else { return res.status(400).send('Invalid request no file or url') } - if (result && result.error) { + if (result?.error) { return res.status(400).send(result.error) - } else if (!result || !result.cover) { + } else if (!result?.cover) { return res.status(500).send('Unknown error occurred') } diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js index f30c9c6d9..934deaff4 100644 --- a/server/managers/CoverManager.js +++ b/server/managers/CoverManager.js @@ -120,13 +120,16 @@ class CoverManager { await fs.ensureDir(coverDirPath) var temppath = Path.posix.join(coverDirPath, 'cover') - var success = await downloadFile(url, temppath).then(() => true).catch((err) => { - Logger.error(`[CoverManager] Download image file failed for "${url}"`, err) + + let errorMsg = '' + let success = await downloadFile(url, temppath).then(() => true).catch((err) => { + errorMsg = err.message || 'Unknown error' + Logger.error(`[CoverManager] Download image file failed for "${url}"`, errorMsg) return false }) if (!success) { return { - error: 'Failed to download image from url' + error: 'Failed to download image from url: ' + errorMsg } } diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 966c7a939..37e890298 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -1,7 +1,8 @@ -const fs = require('../libs/fsExtra') -const rra = require('../libs/recursiveReaddirAsync') const axios = require('axios') const Path = require('path') +const ssrfFilter = require('ssrf-req-filter') +const fs = require('../libs/fsExtra') +const rra = require('../libs/recursiveReaddirAsync') const Logger = require('../Logger') const { AudioMimeType } = require('./constants') @@ -210,7 +211,9 @@ module.exports.downloadFile = (url, filepath) => { url, method: 'GET', responseType: 'stream', - timeout: 30000 + timeout: 30000, + httpAgent: ssrfFilter(url), + httpsAgent: ssrfFilter(url) }).then((response) => { const writer = fs.createWriteStream(filepath) response.data.pipe(writer) From 656c81a1fa6a0d599df7fac77b5cfbe7431d6b62 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 13 Oct 2023 17:37:37 -0500 Subject: [PATCH 054/208] Update:Remove image path input from author modal, add API endpoints for uploading and removing author image --- .../components/modals/authors/EditModal.vue | 92 ++++++++++------ server/controllers/AuthorController.js | 103 +++++++++++++----- server/finders/AuthorFinder.js | 45 ++++---- server/routers/ApiRouter.js | 2 + 4 files changed, 162 insertions(+), 80 deletions(-) diff --git a/client/components/modals/authors/EditModal.vue b/client/components/modals/authors/EditModal.vue index 3af642498..a4fb48a2a 100644 --- a/client/components/modals/authors/EditModal.vue +++ b/client/components/modals/authors/EditModal.vue @@ -5,18 +5,23 @@

{{ title }}

-
-
-
-
-
- -
- delete -
+
+
+
+
+ +
+ delete
-
+
+
+ + + {{ $strings.ButtonSubmit }} + + +
@@ -25,9 +30,9 @@
-
+
@@ -39,9 +44,9 @@ {{ $strings.ButtonSave }}
-
+
- +
@@ -53,9 +58,9 @@ export default { authorCopy: { name: '', asin: '', - description: '', - imagePath: '' + description: '' }, + imageUrl: '', processing: false } }, @@ -100,10 +105,10 @@ export default { }, methods: { init() { + this.imageUrl = '' this.authorCopy.name = this.author.name this.authorCopy.asin = this.author.asin this.authorCopy.description = this.author.description - this.authorCopy.imagePath = this.author.imagePath }, removeClick() { const payload = { @@ -131,7 +136,7 @@ export default { this.$store.commit('globals/setConfirmPrompt', payload) }, async submitForm() { - var keysToCheck = ['name', 'asin', 'description', 'imagePath'] + var keysToCheck = ['name', 'asin', 'description'] var updatePayload = {} keysToCheck.forEach((key) => { if (this.authorCopy[key] !== this.author[key]) { @@ -160,21 +165,46 @@ export default { } this.processing = false }, - async removeCover() { - var updatePayload = { - imagePath: null - } + removeCover() { this.processing = true - var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => { - console.error('Failed', error) - this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed) - return null - }) - if (result && result.updated) { - this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess) - this.$store.commit('globals/showEditAuthorModal', result.author) + this.$axios + .$delete(`/api/authors/${this.authorId}/image`) + .then((data) => { + this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess) + this.$store.commit('globals/showEditAuthorModal', data.author) + }) + .catch((error) => { + console.error('Failed', error) + this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed) + }) + .finally(() => { + this.processing = false + }) + }, + submitUploadCover() { + if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) { + this.$toast.error('Invalid image url') + return } - this.processing = false + + this.processing = true + const updatePayload = { + url: this.imageUrl + } + this.$axios + .$post(`/api/authors/${this.authorId}/image`, updatePayload) + .then((data) => { + this.imageUrl = '' + this.$toast.success('Author image updated') + this.$store.commit('globals/showEditAuthorModal', data.author) + }) + .catch((error) => { + console.error('Failed', error) + this.$toast.error(error.response.data || 'Failed to remove author image') + }) + .finally(() => { + this.processing = false + }) }, async searchAuthor() { if (!this.authorCopy.name && !this.authorCopy.asin) { diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 0cd243fd6..62a7ebde9 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -67,30 +67,10 @@ class AuthorController { const payload = req.body let hasUpdated = false - // Updating/removing cover image - if (payload.imagePath !== undefined && payload.imagePath !== req.author.imagePath) { - if (!payload.imagePath && req.author.imagePath) { // If removing image then remove file - await CacheManager.purgeImageCache(req.author.id) // Purge cache - await CoverManager.removeFile(req.author.imagePath) - } else if (payload.imagePath.startsWith('http')) { // Check if image path is a url - const imageData = await AuthorFinder.saveAuthorImage(req.author.id, payload.imagePath) - if (imageData) { - if (req.author.imagePath) { - await CacheManager.purgeImageCache(req.author.id) // Purge cache - } - payload.imagePath = imageData.path - hasUpdated = true - } - } else if (payload.imagePath && payload.imagePath !== req.author.imagePath) { // Changing image path locally - if (!await fs.pathExists(payload.imagePath)) { // Make sure image path exists - Logger.error(`[AuthorController] Image path does not exist: "${payload.imagePath}"`) - return res.status(400).send('Author image path does not exist') - } - - if (req.author.imagePath) { - await CacheManager.purgeImageCache(req.author.id) // Purge cache - } - } + // author imagePath must be set through other endpoints as of v2.4.5 + if (payload.imagePath !== undefined) { + Logger.warn(`[AuthorController] Updating local author imagePath is not supported`) + delete payload.imagePath } const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name @@ -131,7 +111,7 @@ class AuthorController { Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id) // Send updated num books for merged author - const numBooks = await Database.libraryItemModel.getForAuthor(existingAuthor).length + const numBooks = (await Database.libraryItemModel.getForAuthor(existingAuthor)).length SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks)) res.json({ @@ -191,6 +171,75 @@ class AuthorController { res.sendStatus(200) } + /** + * POST: /api/authors/:id/image + * Upload author image from web URL + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async uploadImage(req, res) { + if (!req.user.canUpload) { + Logger.warn('User attempted to upload an image without permission', req.user) + return res.sendStatus(403) + } + if (!req.body.url) { + Logger.error(`[AuthorController] Invalid request payload. 'url' not in request body`) + return res.status(400).send(`Invalid request payload. 'url' not in request body`) + } + if (!req.body.url.startsWith?.('http:') && !req.body.url.startsWith?.('https:')) { + Logger.error(`[AuthorController] Invalid request payload. Invalid url "${req.body.url}"`) + return res.status(400).send(`Invalid request payload. Invalid url "${req.body.url}"`) + } + + Logger.debug(`[AuthorController] Requesting download author image from url "${req.body.url}"`) + const result = await AuthorFinder.saveAuthorImage(req.author.id, req.body.url) + + if (result?.error) { + return res.status(400).send(result.error) + } else if (!result?.path) { + return res.status(500).send('Unknown error occurred') + } + + if (req.author.imagePath) { + await CacheManager.purgeImageCache(req.author.id) // Purge cache + } + + req.author.imagePath = result.path + await Database.authorModel.updateFromOld(req.author) + + const numBooks = (await Database.libraryItemModel.getForAuthor(req.author)).length + SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) + res.json({ + author: req.author.toJSON() + }) + } + + /** + * DELETE: /api/authors/:id/image + * Remove author image & delete image file + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async deleteImage(req, res) { + if (!req.author.imagePath) { + Logger.error(`[AuthorController] Author "${req.author.imagePath}" has no imagePath set`) + return res.status(400).send('Author has no image path set') + } + Logger.info(`[AuthorController] Removing image for author "${req.author.name}" at "${req.author.imagePath}"`) + await CacheManager.purgeImageCache(req.author.id) // Purge cache + await CoverManager.removeFile(req.author.imagePath) + req.author.imagePath = null + await Database.authorModel.updateFromOld(req.author) + + const numBooks = (await Database.libraryItemModel.getForAuthor(req.author)).length + SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) + res.json({ + author: req.author.toJSON() + }) + } + async match(req, res) { let authorData = null const region = req.body.region || 'us' @@ -215,7 +264,7 @@ class AuthorController { await CacheManager.purgeImageCache(req.author.id) const imageData = await AuthorFinder.saveAuthorImage(req.author.id, authorData.image) - if (imageData) { + if (imageData?.path) { req.author.imagePath = imageData.path hasUpdates = true } @@ -231,7 +280,7 @@ class AuthorController { await Database.updateAuthor(req.author) - const numBooks = await Database.libraryItemModel.getForAuthor(req.author).length + const numBooks = (await Database.libraryItemModel.getForAuthor(req.author)).length SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) } diff --git a/server/finders/AuthorFinder.js b/server/finders/AuthorFinder.js index 9c2a3b4f0..59c6ce166 100644 --- a/server/finders/AuthorFinder.js +++ b/server/finders/AuthorFinder.js @@ -10,13 +10,6 @@ class AuthorFinder { this.audnexus = new Audnexus() } - async downloadImage(url, outputPath) { - return downloadFile(url, outputPath).then(() => true).catch((error) => { - Logger.error('[AuthorFinder] Failed to download author image', error) - return null - }) - } - findAuthorByASIN(asin, region) { if (!asin) return null return this.audnexus.findAuthorByASIN(asin, region) @@ -33,28 +26,36 @@ class AuthorFinder { return author } + /** + * Download author image from url and save in authors folder + * + * @param {string} authorId + * @param {string} url + * @returns {Promise<{path:string, error:string}>} + */ async saveAuthorImage(authorId, url) { - var authorDir = Path.join(global.MetadataPath, 'authors') - var relAuthorDir = Path.posix.join('/metadata', 'authors') + const authorDir = Path.join(global.MetadataPath, 'authors') if (!await fs.pathExists(authorDir)) { await fs.ensureDir(authorDir) } - var imageExtension = url.toLowerCase().split('.').pop() - var ext = imageExtension === 'png' ? 'png' : 'jpg' - var filename = authorId + '.' + ext - var outputPath = Path.posix.join(authorDir, filename) - var relPath = Path.posix.join(relAuthorDir, filename) + const imageExtension = url.toLowerCase().split('.').pop() + const ext = imageExtension === 'png' ? 'png' : 'jpg' + const filename = authorId + '.' + ext + const outputPath = Path.posix.join(authorDir, filename) - var success = await this.downloadImage(url, outputPath) - if (!success) { - return null - } - return { - path: outputPath, - relPath - } + return downloadFile(url, outputPath).then(() => { + return { + path: outputPath + } + }).catch((err) => { + let errorMsg = err.message || 'Unknown error' + Logger.error(`[AuthorFinder] Download image file failed for "${url}"`, errorMsg) + return { + error: errorMsg + } + }) } } module.exports = new AuthorFinder() \ No newline at end of file diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index dc816b44e..a90d18733 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -202,6 +202,8 @@ class ApiRouter { this.router.delete('/authors/:id', AuthorController.middleware.bind(this), AuthorController.delete.bind(this)) this.router.post('/authors/:id/match', AuthorController.middleware.bind(this), AuthorController.match.bind(this)) this.router.get('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.getImage.bind(this)) + this.router.post('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.uploadImage.bind(this)) + this.router.delete('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.deleteImage.bind(this)) // // Series Routes From 616ecf77b062186f43a010c00ac94258d7cc074e Mon Sep 17 00:00:00 2001 From: SunX Date: Sat, 14 Oct 2023 20:30:27 +0800 Subject: [PATCH 055/208] Update zh-cn.json Update zh-cn.json --- client/strings/zh-cn.json | 46 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 1d7f90dd8..f5a32ff4f 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -138,7 +138,7 @@ "HeaderRemoveEpisodes": "移除 {0} 剧集", "HeaderRSSFeedGeneral": "RSS 详细信息", "HeaderRSSFeedIsOpen": "RSS 源已打开", - "HeaderRSSFeeds": "RSS Feeds", + "HeaderRSSFeeds": "RSS 订阅", "HeaderSavedMediaProgress": "保存媒体进度", "HeaderSchedule": "计划任务", "HeaderScheduleLibraryScans": "自动扫描媒体库", @@ -186,7 +186,7 @@ "LabelAuthors": "作者", "LabelAutoDownloadEpisodes": "自动下载剧集", "LabelBackToUser": "返回到用户", - "LabelBackupLocation": "Backup Location", + "LabelBackupLocation": "备份位置", "LabelBackupsEnableAutomaticBackups": "启用自动备份", "LabelBackupsEnableAutomaticBackupsHelp": "备份保存到 /metadata/backups", "LabelBackupsMaxBackupSize": "最大备份大小 (GB)", @@ -203,7 +203,7 @@ "LabelClosePlayer": "关闭播放器", "LabelCodec": "编解码", "LabelCollapseSeries": "折叠系列", - "LabelCollection": "Collection", + "LabelCollection": "收藏", "LabelCollections": "收藏", "LabelComplete": "已完成", "LabelConfirmPassword": "确认密码", @@ -225,9 +225,9 @@ "LabelDirectory": "目录", "LabelDiscFromFilename": "从文件名获取光盘", "LabelDiscFromMetadata": "从元数据获取光盘", - "LabelDiscover": "Discover", + "LabelDiscover": "发现", "LabelDownload": "下载", - "LabelDownloadNEpisodes": "Download {0} episodes", + "LabelDownloadNEpisodes": "下载 {0} 集", "LabelDuration": "持续时间", "LabelDurationFound": "找到持续时间:", "LabelEbook": "电子书", @@ -266,7 +266,7 @@ "LabelHost": "主机", "LabelHour": "小时", "LabelIcon": "图标", - "LabelImageURLFromTheWeb": "Image URL from the web", + "LabelImageURLFromTheWeb": "来自 Web 图像的 URL", "LabelIncludeInTracklist": "包含在音轨列表中", "LabelIncomplete": "未听完", "LabelInProgress": "正在听", @@ -323,7 +323,7 @@ "LabelNewPassword": "新密码", "LabelNextBackupDate": "下次备份日期", "LabelNextScheduledRun": "下次任务运行", - "LabelNoEpisodesSelected": "No episodes selected", + "LabelNoEpisodesSelected": "未选择任何剧集", "LabelNotes": "注释", "LabelNotFinished": "未听完", "LabelNotificationAppriseURL": "通知 URL(s)", @@ -383,8 +383,8 @@ "LabelSearchTitle": "搜索标题", "LabelSearchTitleOrASIN": "搜索标题或 ASIN", "LabelSeason": "季", - "LabelSelectAllEpisodes": "Select all episodes", - "LabelSelectEpisodesShowing": "Select {0} episodes showing", + "LabelSelectAllEpisodes": "选择所有剧集", + "LabelSelectEpisodesShowing": "选择正在播放的 {0} 剧集", "LabelSendEbookToDevice": "发送电子书到...", "LabelSequence": "序列", "LabelSeries": "系列", @@ -400,15 +400,15 @@ "LabelSettingsDisableWatcher": "禁用监视程序", "LabelSettingsDisableWatcherForLibrary": "禁用媒体库的文件夹监视程序", "LabelSettingsDisableWatcherHelp": "检测到文件更改时禁用自动添加和更新项目. *需要重启服务器", - "LabelSettingsEnableWatcher": "Enable Watcher", - "LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library", - "LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart", + "LabelSettingsEnableWatcher": "启用监视程序", + "LabelSettingsEnableWatcherForLibrary": "为库启用文件夹监视程序", + "LabelSettingsEnableWatcherHelp": "当检测到文件更改时, 启用项目的自动添加/更新. *需要重新启动服务器", "LabelSettingsExperimentalFeatures": "实验功能", "LabelSettingsExperimentalFeaturesHelp": "开发中的功能需要你的反馈并帮助测试. 点击打开 github 讨论.", "LabelSettingsFindCovers": "查找封面", "LabelSettingsFindCoversHelp": "如果你的有声读物在文件夹中没有嵌入封面或封面图像, 扫描将尝试查找封面.
注意: 这将延长扫描时间", - "LabelSettingsHideSingleBookSeries": "Hide single book series", - "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", + "LabelSettingsHideSingleBookSeries": "隐藏单书系列", + "LabelSettingsHideSingleBookSeriesHelp": "只有一本书的系列将从系列页面和主页书架中隐藏.", "LabelSettingsHomePageBookshelfView": "首页使用书架视图", "LabelSettingsLibraryBookshelfView": "媒体库使用书架视图", "LabelSettingsParseSubtitles": "解析副标题", @@ -477,7 +477,7 @@ "LabelTrackFromMetadata": "从源数据获取音轨", "LabelTracks": "音轨", "LabelTracksMultiTrack": "多轨", - "LabelTracksNone": "No tracks", + "LabelTracksNone": "没有音轨", "LabelTracksSingleTrack": "单轨", "LabelType": "类型", "LabelUnabridged": "未删节", @@ -518,20 +518,20 @@ "MessageChapterErrorStartLtPrev": "无效的开始时间, 必须大于或等于上一章节的开始时间", "MessageChapterStartIsAfter": "章节开始是在有声读物结束之后", "MessageCheckingCron": "检查计划任务...", - "MessageConfirmCloseFeed": "Are you sure you want to close this feed?", + "MessageConfirmCloseFeed": "你确定要关闭此订阅源吗?", "MessageConfirmDeleteBackup": "你确定要删除备份 {0}?", "MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?", "MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?", "MessageConfirmDeleteSession": "你确定要删除此会话吗?", "MessageConfirmForceReScan": "你确定要强制重新扫描吗?", - "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", - "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", + "MessageConfirmMarkAllEpisodesFinished": "你确定要将所有剧集都标记为已完成吗?", + "MessageConfirmMarkAllEpisodesNotFinished": "你确定要将所有剧集都标记为未完成吗?", "MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?", "MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?", "MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?", - "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", - "MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?", - "MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?", + "MessageConfirmRemoveAuthor": "你确定要删除作者 \"{0}\"?", + "MessageConfirmRemoveCollection": "你确定要移除收藏 \"{0}\"?", + "MessageConfirmRemoveEpisode": "你确定要移除剧集 \"{0}\"?", "MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?", "MessageConfirmRemoveNarrator": "你确定要删除演播者 \"{0}\"?", "MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?", @@ -560,8 +560,8 @@ "MessageM4BFailed": "M4B 失败!", "MessageM4BFinished": "M4B 完成!", "MessageMapChapterTitles": "将章节标题映射到现有的有声读物章节, 无需调整时间戳", - "MessageMarkAllEpisodesFinished": "Mark all episodes finished", - "MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished", + "MessageMarkAllEpisodesFinished": "标记所有剧集为已完成", + "MessageMarkAllEpisodesNotFinished": "标记所有剧集为未完成", "MessageMarkAsFinished": "标记为已听完", "MessageMarkAsNotFinished": "标记为未听完", "MessageMatchBooksDescription": "尝试将媒体库中的图书与所选搜索提供商的图书进行匹配, 并填写空白的详细信息和封面. 不覆盖详细信息.", From c98fac30b6040c8822f8f737ce90590f7a79d95a Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 14 Oct 2023 10:52:56 -0500 Subject: [PATCH 056/208] Update:Validate image URI content-type before writing image file --- server/finders/AuthorFinder.js | 4 ++-- server/managers/CoverManager.js | 6 +++--- server/utils/fileUtils.js | 32 +++++++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/server/finders/AuthorFinder.js b/server/finders/AuthorFinder.js index 59c6ce166..69aa724d5 100644 --- a/server/finders/AuthorFinder.js +++ b/server/finders/AuthorFinder.js @@ -3,7 +3,7 @@ const Logger = require('../Logger') const Path = require('path') const Audnexus = require('../providers/Audnexus') -const { downloadFile } = require('../utils/fileUtils') +const { downloadImageFile } = require('../utils/fileUtils') class AuthorFinder { constructor() { @@ -45,7 +45,7 @@ class AuthorFinder { const filename = authorId + '.' + ext const outputPath = Path.posix.join(authorDir, filename) - return downloadFile(url, outputPath).then(() => { + return downloadImageFile(url, outputPath).then(() => { return { path: outputPath } diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js index 934deaff4..3cf97f339 100644 --- a/server/managers/CoverManager.js +++ b/server/managers/CoverManager.js @@ -5,7 +5,7 @@ const readChunk = require('../libs/readChunk') const imageType = require('../libs/imageType') const globals = require('../utils/globals') -const { downloadFile, filePathToPOSIX, checkPathIsFile } = require('../utils/fileUtils') +const { downloadImageFile, filePathToPOSIX, checkPathIsFile } = require('../utils/fileUtils') const { extractCoverArt } = require('../utils/ffmpegHelpers') const CacheManager = require('../managers/CacheManager') @@ -122,7 +122,7 @@ class CoverManager { var temppath = Path.posix.join(coverDirPath, 'cover') let errorMsg = '' - let success = await downloadFile(url, temppath).then(() => true).catch((err) => { + let success = await downloadImageFile(url, temppath).then(() => true).catch((err) => { errorMsg = err.message || 'Unknown error' Logger.error(`[CoverManager] Download image file failed for "${url}"`, errorMsg) return false @@ -287,7 +287,7 @@ class CoverManager { await fs.ensureDir(coverDirPath) const temppath = Path.posix.join(coverDirPath, 'cover') - const success = await downloadFile(url, temppath).then(() => true).catch((err) => { + const success = await downloadImageFile(url, temppath).then(() => true).catch((err) => { Logger.error(`[CoverManager] Download image file failed for "${url}"`, err) return false }) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 37e890298..4df264004 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -204,7 +204,16 @@ async function recurseFiles(path, relPathToReplace = null) { } module.exports.recurseFiles = recurseFiles -module.exports.downloadFile = (url, filepath) => { +/** + * Download file from web to local file system + * Uses SSRF filter to prevent internal URLs + * + * @param {string} url + * @param {string} filepath path to download the file to + * @param {Function} [contentTypeFilter] validate content type before writing + * @returns {Promise} + */ +module.exports.downloadFile = (url, filepath, contentTypeFilter = null) => { return new Promise(async (resolve, reject) => { Logger.debug(`[fileUtils] Downloading file to ${filepath}`) axios({ @@ -215,6 +224,12 @@ module.exports.downloadFile = (url, filepath) => { httpAgent: ssrfFilter(url), httpsAgent: ssrfFilter(url) }).then((response) => { + // Validate content type + if (contentTypeFilter && !contentTypeFilter?.(response.headers?.['content-type'])) { + return reject(new Error(`Invalid content type "${response.headers?.['content-type'] || ''}"`)) + } + + // Write to filepath const writer = fs.createWriteStream(filepath) response.data.pipe(writer) @@ -227,6 +242,21 @@ module.exports.downloadFile = (url, filepath) => { }) } +/** + * Download image file from web to local file system + * Response header must have content-type of image/ (excluding svg) + * + * @param {string} url + * @param {string} filepath + * @returns {Promise} + */ +module.exports.downloadImageFile = (url, filepath) => { + const contentTypeFilter = (contentType) => { + return contentType?.startsWith('image/') && contentType !== 'image/svg+xml' + } + return this.downloadFile(url, filepath, contentTypeFilter) +} + module.exports.sanitizeFilename = (filename, colonReplacement = ' - ') => { if (typeof filename !== 'string') { return false From dcdd4bb20b26a6830512df7498130fc0781378f0 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 14 Oct 2023 12:50:48 -0500 Subject: [PATCH 057/208] Update:HLS router request validation, smooth out transcode reset logic --- server/objects/Stream.js | 22 +++++++------ server/routers/HlsRouter.js | 65 +++++++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/server/objects/Stream.js b/server/objects/Stream.js index 115bb96e6..2ee66182a 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -101,7 +101,6 @@ class Stream extends EventEmitter { return 'mpegts' } get segmentBasename() { - if (this.hlsSegmentType === 'fmp4') return 'output-%d.m4s' return 'output-%d.ts' } get segmentStartNumber() { @@ -142,19 +141,21 @@ class Stream extends EventEmitter { async checkSegmentNumberRequest(segNum) { const segStartTime = segNum * this.segmentLength - if (this.startTime > segStartTime) { - Logger.warn(`[STREAM] Segment #${segNum} Request @${secondsToTimestamp(segStartTime)} is before start time (${secondsToTimestamp(this.startTime)}) - Reset Transcode`) - await this.reset(segStartTime - (this.segmentLength * 2)) + if (this.segmentStartNumber > segNum) { + Logger.warn(`[STREAM] Segment #${segNum} Request is before starting segment number #${this.segmentStartNumber} - Reset Transcode`) + await this.reset(segStartTime - (this.segmentLength * 5)) return segStartTime } else if (this.isTranscodeComplete) { return false } - const distanceFromFurthestSegment = segNum - this.furthestSegmentCreated - if (distanceFromFurthestSegment > 10) { - Logger.info(`Segment #${segNum} requested is ${distanceFromFurthestSegment} segments from latest (${secondsToTimestamp(segStartTime)}) - Reset Transcode`) - await this.reset(segStartTime - (this.segmentLength * 2)) - return segStartTime + if (this.furthestSegmentCreated) { + const distanceFromFurthestSegment = segNum - this.furthestSegmentCreated + if (distanceFromFurthestSegment > 10) { + Logger.info(`Segment #${segNum} requested is ${distanceFromFurthestSegment} segments from latest (${secondsToTimestamp(segStartTime)}) - Reset Transcode`) + await this.reset(segStartTime - (this.segmentLength * 5)) + return segStartTime + } } return false @@ -171,7 +172,7 @@ class Stream extends EventEmitter { var files = await fs.readdir(this.streamPath) files.forEach((file) => { var extname = Path.extname(file) - if (extname === '.ts' || extname === '.m4s') { + if (extname === '.ts') { var basename = Path.basename(file, extname) var num_part = basename.split('-')[1] var part_num = Number(num_part) @@ -251,6 +252,7 @@ class Stream extends EventEmitter { Logger.info(`[STREAM] START STREAM - Num Segments: ${this.numSegments}`) this.ffmpeg = Ffmpeg() + this.furthestSegmentCreated = 0 var adjustedStartTime = Math.max(this.startTime - this.maxSeekBackTime, 0) var trackStartTime = await writeConcatFile(this.tracks, this.concatFilesPath, adjustedStartTime) diff --git a/server/routers/HlsRouter.js b/server/routers/HlsRouter.js index d4f1bc60c..711e360a2 100644 --- a/server/routers/HlsRouter.js +++ b/server/routers/HlsRouter.js @@ -27,28 +27,60 @@ class HlsRouter { return Number(num_part) } - async streamFileRequest(req, res) { - var streamId = req.params.stream - var fullFilePath = Path.join(this.playbackSessionManager.StreamsPath, streamId, req.params.file) + /** + * Ensure filepath is inside streamDir + * Used to prevent arbitrary file reads + * @see https://nodejs.org/api/path.html#pathrelativefrom-to + * + * @param {string} streamDir + * @param {string} filepath + * @returns {boolean} + */ + validateStreamFilePath(streamDir, filepath) { + const relative = Path.relative(streamDir, filepath) + return relative && !relative.startsWith('..') && !Path.isAbsolute(relative) + } - var exists = await fs.pathExists(fullFilePath) - if (!exists) { + /** + * GET /hls/:stream/:file + * File must have extname .ts or .m3u8 + * + * @param {express.Request} req + * @param {express.Response} res + */ + async streamFileRequest(req, res) { + const streamId = req.params.stream + // Ensure stream is open + const stream = this.playbackSessionManager.getStream(streamId) + if (!stream) { + Logger.error(`[HlsRouter] Stream "${streamId}" does not exist`) + return res.sendStatus(404) + } + + // Ensure stream filepath is valid + const streamDir = Path.join(this.playbackSessionManager.StreamsPath, streamId) + const fullFilePath = Path.join(streamDir, req.params.file) + if (!this.validateStreamFilePath(streamDir, fullFilePath)) { + Logger.error(`[HlsRouter] Invalid file parameter "${req.params.file}"`) + return res.sendStatus(400) + } + + const fileExt = Path.extname(req.params.file) + if (fileExt !== '.ts' && fileExt !== '.m3u8') { + Logger.error(`[HlsRouter] Invalid file parameter "${req.params.file}" extname. Must be .ts or .m3u8`) + return res.sendStatus(400) + } + + if (!(await fs.pathExists(fullFilePath))) { Logger.warn('File path does not exist', fullFilePath) - var fileExt = Path.extname(req.params.file) - if (fileExt === '.ts' || fileExt === '.m4s') { - var segNum = this.parseSegmentFilename(req.params.file) - var stream = this.playbackSessionManager.getStream(streamId) - if (!stream) { - Logger.error(`[HlsRouter] Stream ${streamId} does not exist`) - return res.sendStatus(500) - } + if (fileExt === '.ts') { + const segNum = this.parseSegmentFilename(req.params.file) if (stream.isResetting) { Logger.info(`[HlsRouter] Stream ${streamId} is currently resetting`) - return res.sendStatus(404) } else { - var startTimeForReset = await stream.checkSegmentNumberRequest(segNum) + const startTimeForReset = await stream.checkSegmentNumberRequest(segNum) if (startTimeForReset) { // HLS.js will restart the stream at the new time Logger.info(`[HlsRouter] Resetting Stream - notify client @${startTimeForReset}s`) @@ -56,13 +88,12 @@ class HlsRouter { startTime: startTimeForReset, streamId: stream.id }) - return res.sendStatus(500) } } } + return res.sendStatus(404) } - // Logger.info('Sending file', fullFilePath) res.sendFile(fullFilePath) } } From 07ad81969ced69a94c7eabedb75e6699a182dc71 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 14 Oct 2023 15:04:16 -0500 Subject: [PATCH 058/208] Update:Scanner recognizes asin in book folder names #1852 --- server/scanner/AbsMetadataFileScanner.js | 2 +- server/scanner/LibraryItemScanData.js | 2 +- server/utils/scandir.js | 71 +++++++++++++++++++----- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/server/scanner/AbsMetadataFileScanner.js b/server/scanner/AbsMetadataFileScanner.js index d9d077c00..037726f6f 100644 --- a/server/scanner/AbsMetadataFileScanner.js +++ b/server/scanner/AbsMetadataFileScanner.js @@ -55,7 +55,7 @@ class AbsMetadataFileScanner { bookMetadata.chapters = abMetadata.chapters } for (const key in abMetadata.metadata) { - if (abMetadata.metadata[key] === undefined) continue + if (abMetadata.metadata[key] === undefined || abMetadata.metadata[key] === null) continue bookMetadata[key] = abMetadata.metadata[key] } } diff --git a/server/scanner/LibraryItemScanData.js b/server/scanner/LibraryItemScanData.js index c272127fd..576280c83 100644 --- a/server/scanner/LibraryItemScanData.js +++ b/server/scanner/LibraryItemScanData.js @@ -309,7 +309,7 @@ class LibraryItemScanData { * @param {Object} bookMetadata */ setBookMetadataFromFilenames(bookMetadata) { - const keysToMap = ['title', 'subtitle', 'publishedYear'] + const keysToMap = ['title', 'subtitle', 'publishedYear', 'asin'] for (const key in this.mediaMetadata) { if (keysToMap.includes(key) && this.mediaMetadata[key]) { bookMetadata[key] = this.mediaMetadata[key] diff --git a/server/utils/scandir.js b/server/utils/scandir.js index df6639e04..21c28b8cf 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -8,6 +8,7 @@ const parseNameString = require('./parsers/parseNameString') * @typedef LibraryItemFilenameMetadata * @property {string} title * @property {string} subtitle Book mediaType only + * @property {string} asin Book mediaType only * @property {string[]} authors Book mediaType only * @property {string[]} narrators Book mediaType only * @property {string} seriesName Book mediaType only @@ -237,14 +238,17 @@ function getBookDataFromDir(relPath, parseSubtitle = false) { author = (splitDir.length > 0) ? splitDir.pop() : null // There could be many more directories, but only the top 3 are used for naming /author/series/title/ // The may contain various other pieces of metadata, these functions extract it. + var [folder, asin] = getASIN(folder) var [folder, narrators] = getNarrator(folder) var [folder, sequence] = series ? getSequence(folder) : [folder, null] var [folder, publishedYear] = getPublishedYear(folder) var [title, subtitle] = parseSubtitle ? getSubtitle(folder) : [folder, null] + return { title, subtitle, + asin, authors: parseNameString.parse(author)?.names || [], narrators: parseNameString.parse(narrators)?.names || [], seriesName: series, @@ -254,27 +258,36 @@ function getBookDataFromDir(relPath, parseSubtitle = false) { } module.exports.getBookDataFromDir = getBookDataFromDir +/** + * Extract narrator from folder name + * + * @param {string} folder + * @returns {[string, string]} [folder, narrator] + */ function getNarrator(folder) { let pattern = /^(?.*) \{(?<narrators>.*)\}$/ let match = folder.match(pattern) return match ? [match.groups.title, match.groups.narrators] : [folder, null] } +/** + * Extract series sequence from folder name + * + * @example + * 'Book 2 - Title - Subtitle' + * 'Title - Subtitle - Vol 12' + * 'Title - volume 9 - Subtitle' + * 'Vol. 3 Title Here - Subtitle' + * '1980 - Book 2 - Title' + * 'Volume 12. Title - Subtitle' + * '100 - Book Title' + * '6. Title' + * '0.5 - Book Title' + * + * @param {string} folder + * @returns {[string, string]} [folder, sequence] + */ function getSequence(folder) { - // Valid ways of including a volume number: - // [ - // 'Book 2 - Title - Subtitle', - // 'Title - Subtitle - Vol 12', - // 'Title - volume 9 - Subtitle', - // 'Vol. 3 Title Here - Subtitle', - // '1980 - Book 2 - Title', - // 'Volume 12. Title - Subtitle', - // '100 - Book Title', - // '2 - Book Title', - // '6. Title', - // '0.5 - Book Title' - // ] - // Matches a valid volume string. Also matches a book whose title starts with a 1 to 3 digit number. Will handle that later. let pattern = /^(?<volumeLabel>vol\.? |volume |book )?(?<sequence>\d{0,3}(?:\.\d{1,2})?)(?<trailingDot>\.?)(?: (?<suffix>.*))?$/i @@ -295,6 +308,12 @@ function getSequence(folder) { return [folder, volumeNumber] } +/** + * Extract published year from folder name + * + * @param {string} folder + * @returns {[string, string]} [folder, publishedYear] + */ function getPublishedYear(folder) { var publishedYear = null @@ -308,12 +327,36 @@ function getPublishedYear(folder) { return [folder, publishedYear] } +/** + * Extract subtitle from folder name + * + * @param {string} folder + * @returns {[string, string]} [folder, subtitle] + */ function getSubtitle(folder) { // Subtitle is everything after " - " var splitTitle = folder.split(' - ') return [splitTitle.shift(), splitTitle.join(' - ')] } +/** + * Extract asin from folder name + * + * @param {string} folder + * @returns {[string, string]} [folder, asin] + */ +function getASIN(folder) { + let asin = null + + let pattern = /(?: |^)\[([A-Z0-9]{10})](?= |$)/ // Matches "[B0015T963C]" + const match = folder.match(pattern) + if (match) { + asin = match[1] + folder = folder.replace(match[0], '') + } + return [folder.trim(), asin] +} + /** * * @param {string} relPath From cdd740015c2daa08e870090f35a8e42a28c55042 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 15 Oct 2023 08:23:22 -0500 Subject: [PATCH 059/208] Add:Danish translations --- client/plugins/i18n.js | 1 + client/strings/da.json | 711 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 712 insertions(+) create mode 100644 client/strings/da.json diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 5f193d7bb..f404bb80a 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -5,6 +5,7 @@ import { supplant } from './utils' const defaultCode = 'en-us' const languageCodeMap = { + 'da': { label: 'Dansk', dateFnsLocale: 'da' }, 'de': { label: 'Deutsch', dateFnsLocale: 'de' }, 'en-us': { label: 'English', dateFnsLocale: 'enUS' }, 'es': { label: 'Español', dateFnsLocale: 'es' }, diff --git a/client/strings/da.json b/client/strings/da.json new file mode 100644 index 000000000..359ecdd62 --- /dev/null +++ b/client/strings/da.json @@ -0,0 +1,711 @@ +{ + "ButtonAdd": "Tilføj", + "ButtonAddChapters": "Tilføj kapitler", + "ButtonAddPodcasts": "Tilføj podcasts", + "ButtonAddYourFirstLibrary": "Tilføj din første bibliotek", + "ButtonApply": "Anvend", + "ButtonApplyChapters": "Anvend kapitler", + "ButtonAuthors": "Forfattere", + "ButtonBrowseForFolder": "Gennemse mappe", + "ButtonCancel": "Annuller", + "ButtonCancelEncode": "Annuller kodning", + "ButtonChangeRootPassword": "Ændr rodadgangskode", + "ButtonCheckAndDownloadNewEpisodes": "Tjek og download nye episoder", + "ButtonChooseAFolder": "Vælg en mappe", + "ButtonChooseFiles": "Vælg filer", + "ButtonClearFilter": "Ryd filter", + "ButtonCloseFeed": "Luk feed", + "ButtonCollections": "Samlinger", + "ButtonConfigureScanner": "Konfigurer scanner", + "ButtonCreate": "Opret", + "ButtonCreateBackup": "Opret sikkerhedskopi", + "ButtonDelete": "Slet", + "ButtonDownloadQueue": "Kø", + "ButtonEdit": "Rediger", + "ButtonEditChapters": "Rediger kapitler", + "ButtonEditPodcast": "Rediger podcast", + "ButtonForceReScan": "Tvungen genindlæsning", + "ButtonFullPath": "Fuld sti", + "ButtonHide": "Skjul", + "ButtonHome": "Hjem", + "ButtonIssues": "Problemer", + "ButtonLatest": "Seneste", + "ButtonLibrary": "Bibliotek", + "ButtonLogout": "Log ud", + "ButtonLookup": "Slå op", + "ButtonManageTracks": "Administrer spor", + "ButtonMapChapterTitles": "Kortlæg kapiteloverskrifter", + "ButtonMatchAllAuthors": "Match alle forfattere", + "ButtonMatchBooks": "Match bøger", + "ButtonNevermind": "Glem det", + "ButtonOk": "OK", + "ButtonOpenFeed": "Åbn feed", + "ButtonOpenManager": "Åbn manager", + "ButtonPlay": "Afspil", + "ButtonPlaying": "Afspiller", + "ButtonPlaylists": "Afspilningslister", + "ButtonPurgeAllCache": "Ryd al cache", + "ButtonPurgeItemsCache": "Ryd elementcache", + "ButtonPurgeMediaProgress": "Ryd Medieforløb", + "ButtonQueueAddItem": "Tilføj til kø", + "ButtonQueueRemoveItem": "Fjern fra kø", + "ButtonQuickMatch": "Hurtig Match", + "ButtonRead": "Læs", + "ButtonRemove": "Fjern", + "ButtonRemoveAll": "Fjern Alle", + "ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer", + "ButtonRemoveFromContinueListening": "Fjern fra Fortsæt Lytning", + "ButtonRemoveFromContinueReading": "Fjern fra Fortsæt Læsning", + "ButtonRemoveSeriesFromContinueSeries": "Fjern Serie fra Fortsæt Serie", + "ButtonReScan": "Gen-scan", + "ButtonReset": "Nulstil", + "ButtonRestore": "Gendan", + "ButtonSave": "Gem", + "ButtonSaveAndClose": "Gem & Luk", + "ButtonSaveTracklist": "Gem Sporliste", + "ButtonScan": "Scan", + "ButtonScanLibrary": "Scan Bibliotek", + "ButtonSearch": "Søg", + "ButtonSelectFolderPath": "Vælg Mappen Sti", + "ButtonSeries": "Serie", + "ButtonSetChaptersFromTracks": "Sæt kapitler fra spor", + "ButtonShiftTimes": "Skift Tider", + "ButtonShow": "Vis", + "ButtonStartM4BEncode": "Start M4B Kode", + "ButtonStartMetadataEmbed": "Start Metadata Indlejring", + "ButtonSubmit": "Send", + "ButtonTest": "Test", + "ButtonUpload": "Upload", + "ButtonUploadBackup": "Upload Backup", + "ButtonUploadCover": "Upload Omslag", + "ButtonUploadOPMLFile": "Upload OPML Fil", + "ButtonUserDelete": "Slet bruger {0}", + "ButtonUserEdit": "Rediger bruger {0}", + "ButtonViewAll": "Vis Alle", + "ButtonYes": "Ja", + "HeaderAccount": "Konto", + "HeaderAdvanced": "Avanceret", + "HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger", + "HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer", + "HeaderAudioTracks": "Lydspor", + "HeaderBackups": "Sikkerhedskopier", + "HeaderChangePassword": "Skift Adgangskode", + "HeaderChapters": "Kapitler", + "HeaderChooseAFolder": "Vælg en Mappe", + "HeaderCollection": "Samling", + "HeaderCollectionItems": "Samlingselementer", + "HeaderCover": "Omslag", + "HeaderCurrentDownloads": "Nuværende Downloads", + "HeaderDetails": "Detaljer", + "HeaderDownloadQueue": "Download Kø", + "HeaderEbookFiles": "E-bogsfiler", + "HeaderEmail": "Email", + "HeaderEmailSettings": "Email Indstillinger", + "HeaderEpisodes": "Episoder", + "HeaderEreaderDevices": "E-læser Enheder", + "HeaderEreaderSettings": "E-læser Indstillinger", + "HeaderFiles": "Filer", + "HeaderFindChapters": "Find Kapitler", + "HeaderIgnoredFiles": "Ignorerede Filer", + "HeaderItemFiles": "Emnefiler", + "HeaderItemMetadataUtils": "Emne Metadata Værktøjer", + "HeaderLastListeningSession": "Seneste Lyttesession", + "HeaderLatestEpisodes": "Seneste episoder", + "HeaderLibraries": "Biblioteker", + "HeaderLibraryFiles": "Biblioteksfiler", + "HeaderLibraryStats": "Biblioteksstatistik", + "HeaderListeningSessions": "Lyttesessioner", + "HeaderListeningStats": "Lyttestatistik", + "HeaderLogin": "Log ind", + "HeaderLogs": "Logs", + "HeaderManageGenres": "Administrer Genrer", + "HeaderManageTags": "Administrer Tags", + "HeaderMapDetails": "Kort Detaljer", + "HeaderMatch": "Match", + "HeaderMetadataToEmbed": "Metadata til indlejring", + "HeaderNewAccount": "Ny Konto", + "HeaderNewLibrary": "Nyt Bibliotek", + "HeaderNotifications": "Meddelelser", + "HeaderOpenRSSFeed": "Åbn RSS Feed", + "HeaderOtherFiles": "Andre Filer", + "HeaderPermissions": "Tilladelser", + "HeaderPlayerQueue": "Afspilningskø", + "HeaderPlaylist": "Afspilningsliste", + "HeaderPlaylistItems": "Afspilningsliste Elementer", + "HeaderPodcastsToAdd": "Podcasts til Tilføjelse", + "HeaderPreviewCover": "Forhåndsvis Omslag", + "HeaderRemoveEpisode": "Fjern Episode", + "HeaderRemoveEpisodes": "Fjern {0} Episoder", + "HeaderRSSFeedGeneral": "RSS Detaljer", + "HeaderRSSFeedIsOpen": "RSS Feed er Åben", + "HeaderRSSFeeds": "RSS Feeds", + "HeaderSavedMediaProgress": "Gemt Medieforløb", + "HeaderSchedule": "Planlæg", + "HeaderScheduleLibraryScans": "Planlæg Automatiske Biblioteksscanninger", + "HeaderSession": "Session", + "HeaderSetBackupSchedule": "Indstil Sikkerhedskopieringsplan", + "HeaderSettings": "Indstillinger", + "HeaderSettingsDisplay": "Skærm", + "HeaderSettingsExperimental": "Eksperimentelle Funktioner", + "HeaderSettingsGeneral": "Generelt", + "HeaderSettingsScanner": "Scanner", + "HeaderSleepTimer": "Søvntimer", + "HeaderStatsLargestItems": "Største Elementer", + "HeaderStatsLongestItems": "Længste Elementer (timer)", + "HeaderStatsMinutesListeningChart": "Minutter Lyttet (sidste 7 dage)", + "HeaderStatsRecentSessions": "Seneste Sessions", + "HeaderStatsTop10Authors": "Top 10 Forfattere", + "HeaderStatsTop5Genres": "Top 5 Genrer", + "HeaderTableOfContents": "Indholdsfortegnelse", + "HeaderTools": "Værktøjer", + "HeaderUpdateAccount": "Opdater Konto", + "HeaderUpdateAuthor": "Opdater Forfatter", + "HeaderUpdateDetails": "Opdater Detaljer", + "HeaderUpdateLibrary": "Opdater Bibliotek", + "HeaderUsers": "Brugere", + "HeaderYourStats": "Dine Statistikker", + "LabelAbridged": "Abridged", + "LabelAccountType": "Kontotype", + "LabelAccountTypeAdmin": "Administrator", + "LabelAccountTypeGuest": "Gæst", + "LabelAccountTypeUser": "Bruger", + "LabelActivity": "Aktivitet", + "LabelAdded": "Tilføjet", + "LabelAddedAt": "Tilføjet Kl.", + "LabelAddToCollection": "Tilføj til Samling", + "LabelAddToCollectionBatch": "Tilføj {0} Bøger til Samling", + "LabelAddToPlaylist": "Tilføj til Afspilningsliste", + "LabelAddToPlaylistBatch": "Tilføj {0} Elementer til Afspilningsliste", + "LabelAll": "Alle", + "LabelAllUsers": "Alle Brugere", + "LabelAlreadyInYourLibrary": "Allerede i dit bibliotek", + "LabelAppend": "Tilføj", + "LabelAuthor": "Forfatter", + "LabelAuthorFirstLast": "Forfatter (Fornavn Efternavn)", + "LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)", + "LabelAuthors": "Forfattere", + "LabelAutoDownloadEpisodes": "Auto Download Episoder", + "LabelBackToUser": "Tilbage til Bruger", + "LabelBackupLocation": "Backup Placering", + "LabelBackupsEnableAutomaticBackups": "Aktivér automatisk sikkerhedskopiering", + "LabelBackupsEnableAutomaticBackupsHelp": "Sikkerhedskopier gemt i /metadata/backups", + "LabelBackupsMaxBackupSize": "Maksimal sikkerhedskopistørrelse (i GB)", + "LabelBackupsMaxBackupSizeHelp": "Som en beskyttelse mod fejlkonfiguration fejler sikkerhedskopier, hvis de overstiger den konfigurerede størrelse.", + "LabelBackupsNumberToKeep": "Antal sikkerhedskopier at beholde", + "LabelBackupsNumberToKeepHelp": "Kun 1 sikkerhedskopi fjernes ad gangen, så hvis du allerede har flere sikkerhedskopier end dette, skal du fjerne dem manuelt.", + "LabelBitrate": "Bitrate", + "LabelBooks": "Bøger", + "LabelChangePassword": "Ændre Adgangskode", + "LabelChannels": "Kanaler", + "LabelChapters": "Kapitler", + "LabelChaptersFound": "fundne kapitler", + "LabelChapterTitle": "Kapitel Titel", + "LabelClosePlayer": "Luk afspiller", + "LabelCodec": "Codec", + "LabelCollapseSeries": "Fold Serie Sammen", + "LabelCollection": "Samling", + "LabelCollections": "Samlinger", + "LabelComplete": "Fuldfør", + "LabelConfirmPassword": "Bekræft Adgangskode", + "LabelContinueListening": "Fortsæt Lytning", + "LabelContinueReading": "Fortsæt Læsning", + "LabelContinueSeries": "Fortsæt Serie", + "LabelCover": "Omslag", + "LabelCoverImageURL": "Omslagsbillede URL", + "LabelCreatedAt": "Oprettet Kl.", + "LabelCronExpression": "Cron Udtryk", + "LabelCurrent": "Aktuel", + "LabelCurrently": "Aktuelt:", + "LabelCustomCronExpression": "Brugerdefineret Cron Udtryk:", + "LabelDatetime": "Dato og Tid", + "LabelDescription": "Beskrivelse", + "LabelDeselectAll": "Fravælg Alle", + "LabelDevice": "Enheds", + "LabelDeviceInfo": "Enhedsinformation", + "LabelDirectory": "Mappe", + "LabelDiscFromFilename": "Disk fra Filnavn", + "LabelDiscFromMetadata": "Disk fra Metadata", + "LabelDiscover": "Opdag", + "LabelDownload": "Download", + "LabelDownloadNEpisodes": "Download {0} episoder", + "LabelDuration": "Varighed", + "LabelDurationFound": "Fundet varighed:", + "LabelEbook": "E-bog", + "LabelEbooks": "E-bøger", + "LabelEdit": "Rediger", + "LabelEmail": "Email", + "LabelEmailSettingsFromAddress": "Fra Adresse", + "LabelEmailSettingsSecure": "Sikker", + "LabelEmailSettingsSecureHelp": "Hvis sandt, vil forbindelsen bruge TLS ved tilslutning til serveren. Hvis falsk, bruges TLS, hvis serveren understøtter STARTTLS-udvidelsen. I de fleste tilfælde skal denne værdi sættes til sandt, hvis du tilslutter til port 465. Til port 587 eller 25 skal du holde det falsk. (fra nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Test Adresse", + "LabelEmbeddedCover": "Indlejret Omslag", + "LabelEnable": "Aktivér", + "LabelEnd": "Slut", + "LabelEpisode": "Episode", + "LabelEpisodeTitle": "Episodetitel", + "LabelEpisodeType": "Episodetype", + "LabelExample": "Eksempel", + "LabelExplicit": "Eksplisit", + "LabelFeedURL": "Feed URL", + "LabelFile": "Fil", + "LabelFileBirthtime": "Fødselstidspunkt for fil", + "LabelFileModified": "Fil ændret", + "LabelFilename": "Filnavn", + "LabelFilterByUser": "Filtrér efter bruger", + "LabelFindEpisodes": "Find episoder", + "LabelFinished": "Færdig", + "LabelFolder": "Mappe", + "LabelFolders": "Mapper", + "LabelFontScale": "Skriftstørrelse", + "LabelFormat": "Format", + "LabelGenre": "Genre", + "LabelGenres": "Genrer", + "LabelHardDeleteFile": "Permanent slet fil", + "LabelHasEbook": "Har e-bog", + "LabelHasSupplementaryEbook": "Har supplerende e-bog", + "LabelHost": "Vært", + "LabelHour": "Time", + "LabelIcon": "Ikon", + "LabelImageURLFromTheWeb": "Image URL from the web", + "LabelIncludeInTracklist": "Inkluder i afspilningsliste", + "LabelIncomplete": "Ufuldstændig", + "LabelInProgress": "I gang", + "LabelInterval": "Interval", + "LabelIntervalCustomDailyWeekly": "Tilpasset dagligt/ugentligt", + "LabelIntervalEvery12Hours": "Hver 12. time", + "LabelIntervalEvery15Minutes": "Hver 15. minut", + "LabelIntervalEvery2Hours": "Hver 2. time", + "LabelIntervalEvery30Minutes": "Hver 30. minut", + "LabelIntervalEvery6Hours": "Hver 6. time", + "LabelIntervalEveryDay": "Hver dag", + "LabelIntervalEveryHour": "Hver time", + "LabelInvalidParts": "Ugyldige dele", + "LabelInvert": "Inverter", + "LabelItem": "Element", + "LabelLanguage": "Sprog", + "LabelLanguageDefaultServer": "Standard server sprog", + "LabelLastBookAdded": "Senest tilføjede bog", + "LabelLastBookUpdated": "Senest opdaterede bog", + "LabelLastSeen": "Sidst set", + "LabelLastTime": "Sidste gang", + "LabelLastUpdate": "Seneste opdatering", + "LabelLayout": "Layout", + "LabelLayoutSinglePage": "Enkeltside", + "LabelLayoutSplitPage": "Opdelt side", + "LabelLess": "Mindre", + "LabelLibrariesAccessibleToUser": "Biblioteker tilgængelige for bruger", + "LabelLibrary": "Bibliotek", + "LabelLibraryItem": "Bibliotekselement", + "LabelLibraryName": "Biblioteksnavn", + "LabelLimit": "Grænse", + "LabelLineSpacing": "Linjeafstand", + "LabelListenAgain": "Lyt igen", + "LabelLogLevelDebug": "Fejlsøgning", + "LabelLogLevelInfo": "Information", + "LabelLogLevelWarn": "Advarsel", + "LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato", + "LabelMediaPlayer": "Medieafspiller", + "LabelMediaType": "Medietype", + "LabelMetadataProvider": "Metadataudbyder", + "LabelMetaTag": "Meta-tag", + "LabelMetaTags": "Meta-tags", + "LabelMinute": "Minut", + "LabelMissing": "Mangler", + "LabelMissingParts": "Manglende dele", + "LabelMore": "Mere", + "LabelMoreInfo": "Mere info", + "LabelName": "Navn", + "LabelNarrator": "Fortæller", + "LabelNarrators": "Fortællere", + "LabelNew": "Ny", + "LabelNewestAuthors": "Nyeste forfattere", + "LabelNewestEpisodes": "Nyeste episoder", + "LabelNewPassword": "Nyt kodeord", + "LabelNextBackupDate": "Næste sikkerhedskopi dato", + "LabelNextScheduledRun": "Næste planlagte kørsel", + "LabelNoEpisodesSelected": "Ingen episoder valgt", + "LabelNotes": "Noter", + "LabelNotFinished": "Ikke færdig", + "LabelNotificationAppriseURL": "Apprise URL'er", + "LabelNotificationAvailableVariables": "Tilgængelige variabler", + "LabelNotificationBodyTemplate": "Kropsskabelon", + "LabelNotificationEvent": "Meddelelseshændelse", + "LabelNotificationsMaxFailedAttempts": "Maksimalt antal mislykkede forsøg", + "LabelNotificationsMaxFailedAttemptsHelp": "Meddelelser deaktiveres, når de mislykkes med at sende så mange gange", + "LabelNotificationsMaxQueueSize": "Maksimal køstørrelse for meddelelseshændelser", + "LabelNotificationsMaxQueueSizeHelp": "Hændelser begrænses til at udløse en gang pr. sekund. Hændelser ignoreres, hvis køen er fyldt. Dette forhindrer meddelelsesspam.", + "LabelNotificationTitleTemplate": "Titelskabelon", + "LabelNotStarted": "Ikke påbegyndt", + "LabelNumberOfBooks": "Antal bøger", + "LabelNumberOfEpisodes": "Antal episoder", + "LabelOpenRSSFeed": "Åbn RSS-feed", + "LabelOverwrite": "Overskriv", + "LabelPassword": "Kodeord", + "LabelPath": "Sti", + "LabelPermissionsAccessAllLibraries": "Kan få adgang til alle biblioteker", + "LabelPermissionsAccessAllTags": "Kan få adgang til alle tags", + "LabelPermissionsAccessExplicitContent": "Kan få adgang til eksplicit indhold", + "LabelPermissionsDelete": "Kan slette", + "LabelPermissionsDownload": "Kan downloade", + "LabelPermissionsUpdate": "Kan opdatere", + "LabelPermissionsUpload": "Kan uploade", + "LabelPhotoPathURL": "Foto sti/URL", + "LabelPlaylists": "Afspilningslister", + "LabelPlayMethod": "Afspilningsmetode", + "LabelPodcast": "Podcast", + "LabelPodcasts": "Podcasts", + "LabelPodcastType": "Podcast type", + "LabelPort": "Port", + "LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)", + "LabelPreventIndexing": "Forhindrer, at dit feed bliver indekseret af iTunes og Google podcastkataloger", + "LabelPrimaryEbook": "Primær e-bog", + "LabelProgress": "Fremskridt", + "LabelProvider": "Udbyder", + "LabelPubDate": "Udgivelsesdato", + "LabelPublisher": "Forlag", + "LabelPublishYear": "Udgivelsesår", + "LabelRead": "Læst", + "LabelReadAgain": "Læs igen", + "LabelReadEbookWithoutProgress": "Læs e-bog uden at følge fremskridt", + "LabelRecentlyAdded": "Senest tilføjet", + "LabelRecentSeries": "Seneste serie", + "LabelRecommended": "Anbefalet", + "LabelRegion": "Region", + "LabelReleaseDate": "Udgivelsesdato", + "LabelRemoveCover": "Fjern omslag", + "LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail", + "LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn", + "LabelRSSFeedOpen": "Åben RSS-feed", + "LabelRSSFeedPreventIndexing": "Forhindrer indeksering", + "LabelRSSFeedSlug": "RSS-feed-slug", + "LabelRSSFeedURL": "RSS-feed-URL", + "LabelSearchTerm": "Søgeterm", + "LabelSearchTitle": "Søg efter titel", + "LabelSearchTitleOrASIN": "Søg efter titel eller ASIN", + "LabelSeason": "Sæson", + "LabelSelectAllEpisodes": "Vælg alle episoder", + "LabelSelectEpisodesShowing": "Vælg {0} episoder vist", + "LabelSendEbookToDevice": "Send e-bog til...", + "LabelSequence": "Sekvens", + "LabelSeries": "Serie", + "LabelSeriesName": "Serienavn", + "LabelSeriesProgress": "Seriefremskridt", + "LabelSetEbookAsPrimary": "Indstil som primær", + "LabelSetEbookAsSupplementary": "Indstil som supplerende", + "LabelSettingsAudiobooksOnly": "Kun lydbøger", + "LabelSettingsAudiobooksOnlyHelp": "Aktivering af denne indstilling vil ignorere e-bogsfiler, medmindre de er inde i en lydbogmappe, hvor de vil blive indstillet som supplerende e-bøger", + "LabelSettingsBookshelfViewHelp": "Skeumorfisk design med træhylder", + "LabelSettingsChromecastSupport": "Chromecast-understøttelse", + "LabelSettingsDateFormat": "Datoformat", + "LabelSettingsDisableWatcher": "Deaktiver overvågning", + "LabelSettingsDisableWatcherForLibrary": "Deaktiver mappeovervågning for bibliotek", + "LabelSettingsDisableWatcherHelp": "Deaktiverer automatisk tilføjelse/opdatering af elementer, når der registreres filændringer. *Kræver servergenstart", + "LabelSettingsEnableWatcher": "Aktiver overvågning", + "LabelSettingsEnableWatcherForLibrary": "Aktiver mappeovervågning for bibliotek", + "LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart", + "LabelSettingsExperimentalFeatures": "Eksperimentelle funktioner", + "LabelSettingsExperimentalFeaturesHelp": "Funktioner under udvikling, der kunne bruge din feedback og hjælp til test. Klik for at åbne Github-diskussionen.", + "LabelSettingsFindCovers": "Find omslag", + "LabelSettingsFindCoversHelp": "Hvis din lydbog ikke har et indlejret omslag eller et omslagsbillede i mappen, vil skanneren forsøge at finde et omslag.<br>Bemærk: Dette vil forlænge scanntiden", + "LabelSettingsHideSingleBookSeries": "Skjul enkeltbogsserier", + "LabelSettingsHideSingleBookSeriesHelp": "Serier med en enkelt bog vil blive skjult fra serie-siden og hjemmesidehylder.", + "LabelSettingsHomePageBookshelfView": "Brug bogreolvisning på startside", + "LabelSettingsLibraryBookshelfView": "Brug bogreolvisning i biblioteket", + "LabelSettingsParseSubtitles": "Fortolk undertekster", + "LabelSettingsParseSubtitlesHelp": "Udtræk undertekster fra lydbogsmappenavne.<br>Undertitler skal adskilles af \" - \"<br>f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"", + "LabelSettingsPreferMatchedMetadata": "Foretræk matchede metadata", + "LabelSettingsPreferMatchedMetadataHelp": "Matchede data vil tilsidesætte elementdetaljer ved brug af Hurtig Match. Som standard udfylder Hurtig Match kun manglende detaljer.", + "LabelSettingsSkipMatchingBooksWithASIN": "Spring over matchende bøger, der allerede har en ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Spring over matchende bøger, der allerede har en ISBN", + "LabelSettingsSortingIgnorePrefixes": "Ignorer præfikser ved sortering", + "LabelSettingsSortingIgnorePrefixesHelp": "f.eks. for præfikset \"the\" vil bogtitlen \"The Book Title\" blive sorteret som \"Book Title, The\"", + "LabelSettingsSquareBookCovers": "Brug kvadratiske bogomslag", + "LabelSettingsSquareBookCoversHelp": "Foretræk at bruge kvadratiske omslag frem for standard 1,6:1 bogomslag", + "LabelSettingsStoreCoversWithItem": "Gem omslag med element", + "LabelSettingsStoreCoversWithItemHelp": "Som standard gemmes omslag i /metadata/items, aktivering af denne indstilling vil gemme omslag i mappen for dit bibliotekselement. Kun én fil med navnet \"cover\" vil blive bevaret", + "LabelSettingsStoreMetadataWithItem": "Gem metadata med element", + "LabelSettingsStoreMetadataWithItemHelp": "Som standard gemmes metadatafiler i /metadata/items, aktivering af denne indstilling vil gemme metadatafiler i dine bibliotekselementmapper. Bruger .abs-filudvidelsen", + "LabelSettingsTimeFormat": "Tidsformat", + "LabelShowAll": "Vis alle", + "LabelSize": "Størrelse", + "LabelSleepTimer": "Søvntimer", + "LabelSlug": "Slug", + "LabelStart": "Start", + "LabelStarted": "Startet", + "LabelStartedAt": "Startet klokken", + "LabelStartTime": "Starttid", + "LabelStatsAudioTracks": "Lydspor", + "LabelStatsAuthors": "Forfattere", + "LabelStatsBestDay": "Bedste dag", + "LabelStatsDailyAverage": "Daglig gennemsnit", + "LabelStatsDays": "Dage", + "LabelStatsDaysListened": "Dage hørt", + "LabelStatsHours": "Timer", + "LabelStatsInARow": "i træk", + "LabelStatsItemsFinished": "Elementer færdige", + "LabelStatsItemsInLibrary": "Elementer i biblioteket", + "LabelStatsMinutes": "minutter", + "LabelStatsMinutesListening": "Minutter hørt", + "LabelStatsOverallDays": "Samlede dage", + "LabelStatsOverallHours": "Samlede timer", + "LabelStatsWeekListening": "Ugens lytning", + "LabelSubtitle": "Undertekst", + "LabelSupportedFileTypes": "Understøttede filtyper", + "LabelTag": "Mærke", + "LabelTags": "Mærker", + "LabelTagsAccessibleToUser": "Mærker tilgængelige for bruger", + "LabelTagsNotAccessibleToUser": "Mærker ikke tilgængelige for bruger", + "LabelTasks": "Kører opgaver", + "LabelTheme": "Tema", + "LabelThemeDark": "Mørk", + "LabelThemeLight": "Lys", + "LabelTimeBase": "Tidsbase", + "LabelTimeListened": "Tid hørt", + "LabelTimeListenedToday": "Tid hørt i dag", + "LabelTimeRemaining": "{0} tilbage", + "LabelTimeToShift": "Tid til skift i sekunder", + "LabelTitle": "Titel", + "LabelToolsEmbedMetadata": "Indlejre metadata", + "LabelToolsEmbedMetadataDescription": "Indlejr metadata i lydfiler, inklusive omslag og kapitler.", + "LabelToolsMakeM4b": "Lav M4B lydbogsfil", + "LabelToolsMakeM4bDescription": "Generer en .M4B lydbogsfil med indlejret metadata, omslag og kapitler.", + "LabelToolsSplitM4b": "Opdel M4B til MP3'er", + "LabelToolsSplitM4bDescription": "Opret MP3'er fra en M4B opdelt efter kapitler med indlejret metadata, omslag og kapitler.", + "LabelTotalDuration": "Samlet varighed", + "LabelTotalTimeListened": "Samlet lyttetid", + "LabelTrackFromFilename": "Spor fra filnavn", + "LabelTrackFromMetadata": "Spor fra metadata", + "LabelTracks": "Spor", + "LabelTracksMultiTrack": "Flerspors", + "LabelTracksNone": "Ingen spor", + "LabelTracksSingleTrack": "Enkeltspors", + "LabelType": "Type", + "LabelUnabridged": "Uforkortet", + "LabelUnknown": "Ukendt", + "LabelUpdateCover": "Opdater omslag", + "LabelUpdateCoverHelp": "Tillad overskrivning af eksisterende omslag for de valgte bøger, når der findes en match", + "LabelUpdatedAt": "Opdateret ved", + "LabelUpdateDetails": "Opdater detaljer", + "LabelUpdateDetailsHelp": "Tillad overskrivning af eksisterende detaljer for de valgte bøger, når der findes en match", + "LabelUploaderDragAndDrop": "Træk og slip filer eller mapper", + "LabelUploaderDropFiles": "Smid filer", + "LabelUseChapterTrack": "Brug kapitel-spor", + "LabelUseFullTrack": "Brug fuldt spor", + "LabelUser": "Bruger", + "LabelUsername": "Brugernavn", + "LabelValue": "Værdi", + "LabelVersion": "Version", + "LabelViewBookmarks": "Se bogmærker", + "LabelViewChapters": "Se kapitler", + "LabelViewQueue": "Se afspilningskø", + "LabelVolume": "Volumen", + "LabelWeekdaysToRun": "Ugedage til kørsel", + "LabelYourAudiobookDuration": "Din lydbogsvarighed", + "LabelYourBookmarks": "Dine bogmærker", + "LabelYourPlaylists": "Dine spillelister", + "LabelYourProgress": "Din fremgang", + "MessageAddToPlayerQueue": "Tilføj til afspilningskø", + "MessageAppriseDescription": "For at bruge denne funktion skal du have en instans af <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> kørende eller en API, der håndterer de samme anmodninger. <br /> Apprise API-webadressen skal være den fulde URL-sti for at sende underretningen, f.eks. hvis din API-instans er tilgængelig på <code>http://192.168.1.1:8337</code>, så skal du bruge <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "Backups inkluderer brugere, brugerfremskridt, biblioteksvareoplysninger, serverindstillinger og billeder gemt i <code>/metadata/items</code> og <code>/metadata/authors</code>. Backups inkluderer <strong>ikke</strong> nogen filer gemt i dine biblioteksmapper.", + "MessageBatchQuickMatchDescription": "Quick Match vil forsøge at tilføje manglende omslag og metadata til de valgte elementer. Aktivér indstillingerne nedenfor for at tillade Quick Match at overskrive eksisterende omslag og/eller metadata.", + "MessageBookshelfNoCollections": "Du har ikke oprettet nogen samlinger endnu", + "MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "Ingen RSS-feeds er åbne", + "MessageBookshelfNoSeries": "Du har ingen serier", + "MessageChapterEndIsAfter": "Kapitelslutningen er efter slutningen af din lydbog", + "MessageChapterErrorFirstNotZero": "Første kapitel skal starte ved 0", + "MessageChapterErrorStartGteDuration": "Ugyldig starttid skal være mindre end lydbogens varighed", + "MessageChapterErrorStartLtPrev": "Ugyldig starttid skal være større end eller lig med den foregående kapitels starttid", + "MessageChapterStartIsAfter": "Kapitelstarten er efter slutningen af din lydbog", + "MessageCheckingCron": "Tjekker cron...", + "MessageConfirmCloseFeed": "Er du sikker på, at du vil lukke dette feed?", + "MessageConfirmDeleteBackup": "Er du sikker på, at du vil slette backup for {0}?", + "MessageConfirmDeleteFile": "Dette vil slette filen fra dit filsystem. Er du sikker?", + "MessageConfirmDeleteLibrary": "Er du sikker på, at du vil slette biblioteket permanent \"{0}\"?", + "MessageConfirmDeleteSession": "Er du sikker på, at du vil slette denne session?", + "MessageConfirmForceReScan": "Er du sikker på, at du vil tvinge en genindlæsning?", + "MessageConfirmMarkAllEpisodesFinished": "Er du sikker på, at du vil markere alle episoder som afsluttet?", + "MessageConfirmMarkAllEpisodesNotFinished": "Er du sikker på, at du vil markere alle episoder som ikke afsluttet?", + "MessageConfirmMarkSeriesFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som afsluttet?", + "MessageConfirmMarkSeriesNotFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som ikke afsluttet?", + "MessageConfirmRemoveAllChapters": "Er du sikker på, at du vil fjerne alle kapitler?", + "MessageConfirmRemoveAuthor": "Er du sikker på, at du vil fjerne forfatteren \"{0}\"?", + "MessageConfirmRemoveCollection": "Er du sikker på, at du vil fjerne samlingen \"{0}\"?", + "MessageConfirmRemoveEpisode": "Er du sikker på, at du vil fjerne episoden \"{0}\"?", + "MessageConfirmRemoveEpisodes": "Er du sikker på, at du vil fjerne {0} episoder?", + "MessageConfirmRemoveNarrator": "Er du sikker på, at du vil fjerne fortælleren \"{0}\"?", + "MessageConfirmRemovePlaylist": "Er du sikker på, at du vil fjerne din spilleliste \"{0}\"?", + "MessageConfirmRenameGenre": "Er du sikker på, at du vil omdøbe genre \"{0}\" til \"{1}\" for alle elementer?", + "MessageConfirmRenameGenreMergeNote": "Bemærk: Denne genre findes allerede, så de vil blive fusioneret.", + "MessageConfirmRenameGenreWarning": "Advarsel! En lignende genre med en anden skrivemåde eksisterer allerede \"{0}\".", + "MessageConfirmRenameTag": "Er du sikker på, at du vil omdøbe tag \"{0}\" til \"{1}\" for alle elementer?", + "MessageConfirmRenameTagMergeNote": "Bemærk: Dette tag findes allerede, så de vil blive fusioneret.", + "MessageConfirmRenameTagWarning": "Advarsel! Et lignende tag med en anden skrivemåde eksisterer allerede \"{0}\".", + "MessageConfirmSendEbookToDevice": "Er du sikker på, at du vil sende {0} e-bog \"{1}\" til enhed \"{2}\"?", + "MessageDownloadingEpisode": "Downloader episode", + "MessageDragFilesIntoTrackOrder": "Træk filer ind i korrekt spororden", + "MessageEmbedFinished": "Indlejring færdig!", + "MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download", + "MessageFeedURLWillBe": "Feed-URL vil være {0}", + "MessageFetching": "Henter...", + "MessageForceReScanDescription": "vil scanne alle filer igen som en frisk scanning. Lydfilens ID3-tags, OPF-filer og tekstfiler scannes som nye.", + "MessageImportantNotice": "Vigtig besked!", + "MessageInsertChapterBelow": "Indsæt kapitel nedenfor", + "MessageItemsSelected": "{0} elementer valgt", + "MessageItemsUpdated": "{0} elementer opdateret", + "MessageJoinUsOn": "Deltag i os på", + "MessageListeningSessionsInTheLastYear": "{0} lyttesessioner i det sidste år", + "MessageLoading": "Indlæser...", + "MessageLoadingFolders": "Indlæser mapper...", + "MessageM4BFailed": "M4B mislykkedes!", + "MessageM4BFinished": "M4B afsluttet!", + "MessageMapChapterTitles": "Tilknyt kapiteloverskrifter til dine eksisterende lydbogskapitler uden at justere tidsstempler", + "MessageMarkAllEpisodesFinished": "Markér alle episoder som afsluttet", + "MessageMarkAllEpisodesNotFinished": "Markér alle episoder som ikke afsluttet", + "MessageMarkAsFinished": "Markér som afsluttet", + "MessageMarkAsNotFinished": "Markér som ikke afsluttet", + "MessageMatchBooksDescription": "vil forsøge at matche bøger i biblioteket med en bog fra den valgte søgeudbyder og udfylde tomme detaljer og omslag. Overskriver ikke detaljer.", + "MessageNoAudioTracks": "Ingen lydspor", + "MessageNoAuthors": "Ingen forfattere", + "MessageNoBackups": "Ingen sikkerhedskopier", + "MessageNoBookmarks": "Ingen bogmærker", + "MessageNoChapters": "Ingen kapitler", + "MessageNoCollections": "Ingen samlinger", + "MessageNoCoversFound": "Ingen omslag fundet", + "MessageNoDescription": "Ingen beskrivelse", + "MessageNoDownloadsInProgress": "Ingen downloads i gang lige nu", + "MessageNoDownloadsQueued": "Ingen downloads i kø", + "MessageNoEpisodeMatchesFound": "Ingen episode-matcher fundet", + "MessageNoEpisodes": "Ingen episoder", + "MessageNoFoldersAvailable": "Ingen mapper tilgængelige", + "MessageNoGenres": "Ingen genrer", + "MessageNoIssues": "Ingen problemer", + "MessageNoItems": "Ingen elementer", + "MessageNoItemsFound": "Ingen elementer fundet", + "MessageNoListeningSessions": "Ingen lyttesessioner", + "MessageNoLogs": "Ingen logfiler", + "MessageNoMediaProgress": "Ingen medieforløb", + "MessageNoNotifications": "Ingen meddelelser", + "MessageNoPodcastsFound": "Ingen podcasts fundet", + "MessageNoResults": "Ingen resultater", + "MessageNoSearchResultsFor": "Ingen søgeresultater for \"{0}\"", + "MessageNoSeries": "Ingen serier", + "MessageNoTags": "Ingen tags", + "MessageNoTasksRunning": "Ingen opgaver kører", + "MessageNotYetImplemented": "Endnu ikke implementeret", + "MessageNoUpdateNecessary": "Ingen opdatering nødvendig", + "MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige", + "MessageNoUserPlaylists": "Du har ingen afspilningslister", + "MessageOr": "eller", + "MessagePauseChapter": "Pause kapitelafspilning", + "MessagePlayChapter": "Lyt til begyndelsen af kapitlet", + "MessagePlaylistCreateFromCollection": "Opret afspilningsliste fra samling", + "MessagePodcastHasNoRSSFeedForMatching": "Podcast har ingen RSS-feed-URL at bruge til matchning", + "MessageQuickMatchDescription": "Udfyld tomme elementoplysninger og omslag med første matchresultat fra '{0}'. Overskriver ikke oplysninger, medmindre serverindstillingen 'Foretræk matchet metadata' er aktiveret.", + "MessageRemoveChapter": "Fjern kapitel", + "MessageRemoveEpisodes": "Fjern {0} episode(r)", + "MessageRemoveFromPlayerQueue": "Fjern fra afspillingskøen", + "MessageRemoveUserWarning": "Er du sikker på, at du vil slette brugeren permanent \"{0}\"?", + "MessageReportBugsAndContribute": "Rapporter fejl, anmod om funktioner og bidrag på", + "MessageResetChaptersConfirm": "Er du sikker på, at du vil nulstille kapitler og annullere ændringerne, du har foretaget?", + "MessageRestoreBackupConfirm": "Er du sikker på, at du vil gendanne sikkerhedskopien oprettet den", + "MessageRestoreBackupWarning": "Gendannelse af en sikkerhedskopi vil overskrive hele databasen, som er placeret på /config, og omslagsbilleder i /metadata/items & /metadata/authors.<br /><br />Sikkerhedskopier ændrer ikke nogen filer i dine biblioteksmapper. Hvis du har aktiveret serverindstillinger for at gemme omslagskunst og metadata i dine biblioteksmapper, sikkerhedskopieres eller overskrives disse ikke.<br /><br />Alle klienter, der bruger din server, opdateres automatisk.", + "MessageSearchResultsFor": "Søgeresultater for", + "MessageServerCouldNotBeReached": "Serveren kunne ikke nås", + "MessageSetChaptersFromTracksDescription": "Indstil kapitler ved at bruge hver lydfil som et kapitel og kapiteloverskrift som lydfilnavn", + "MessageStartPlaybackAtTime": "Start afspilning for \"{0}\" kl. {1}?", + "MessageThinking": "Tænker...", + "MessageUploaderItemFailed": "Fejl ved upload", + "MessageUploaderItemSuccess": "Uploadet med succes!", + "MessageUploading": "Uploader...", + "MessageValidCronExpression": "Gyldigt cron-udtryk", + "MessageWatcherIsDisabledGlobally": "Watcher er deaktiveret globalt i serverindstillinger", + "MessageXLibraryIsEmpty": "{0} bibliotek er tomt!", + "MessageYourAudiobookDurationIsLonger": "Din lydbogsvarighed er længere end den fundne varighed", + "MessageYourAudiobookDurationIsShorter": "Din lydbogsvarighed er kortere end den fundne varighed", + "NoteChangeRootPassword": "Root-brugeren er den eneste bruger, der kan have en tom adgangskode", + "NoteChapterEditorTimes": "Bemærk: Første kapitel starttidspunkt skal forblive kl. 0:00, og det sidste kapitel starttidspunkt må ikke overstige denne lydbogs varighed.", + "NoteFolderPicker": "Bemærk: Mapper, der allerede er mappet, vises ikke", + "NoteFolderPickerDebian": "Bemærk: Mappicker for Debian-installationen er ikke fuldt implementeret. Du bør indtaste stien til dit bibliotek direkte.", + "NoteRSSFeedPodcastAppsHttps": "Advarsel: De fleste podcast-apps kræver, at RSS-feedets URL bruger HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "Advarsel: En eller flere af dine episoder har ikke en Pub Date. Nogle podcast-apps kræver dette.", + "NoteUploaderFoldersWithMediaFiles": "Mapper med mediefiler håndteres som separate bibliotekselementer.", + "NoteUploaderOnlyAudioFiles": "Hvis du kun uploader lydfiler, håndteres hver lydfil som en separat lydbog.", + "NoteUploaderUnsupportedFiles": "Ikke-understøttede filer ignoreres. Når du vælger eller slipper en mappe, ignoreres andre filer, der ikke er i en emnemappe.", + "PlaceholderNewCollection": "Nyt samlingnavn", + "PlaceholderNewFolderPath": "Ny mappes sti", + "PlaceholderNewPlaylist": "Nyt afspilningslistnavn", + "PlaceholderSearch": "Søg..", + "PlaceholderSearchEpisode": "Søg efter episode..", + "ToastAccountUpdateFailed": "Mislykkedes opdatering af konto", + "ToastAccountUpdateSuccess": "Konto opdateret", + "ToastAuthorImageRemoveFailed": "Mislykkedes fjernelse af forfatterbillede", + "ToastAuthorImageRemoveSuccess": "Forfatterbillede fjernet", + "ToastAuthorUpdateFailed": "Mislykkedes opdatering af forfatter", + "ToastAuthorUpdateMerged": "Forfatter fusioneret", + "ToastAuthorUpdateSuccess": "Forfatter opdateret", + "ToastAuthorUpdateSuccessNoImageFound": "Forfatter opdateret (ingen billede fundet)", + "ToastBackupCreateFailed": "Mislykkedes oprettelse af sikkerhedskopi", + "ToastBackupCreateSuccess": "Sikkerhedskopi oprettet", + "ToastBackupDeleteFailed": "Mislykkedes sletning af sikkerhedskopi", + "ToastBackupDeleteSuccess": "Sikkerhedskopi slettet", + "ToastBackupRestoreFailed": "Mislykkedes gendannelse af sikkerhedskopi", + "ToastBackupUploadFailed": "Mislykkedes upload af sikkerhedskopi", + "ToastBackupUploadSuccess": "Sikkerhedskopi uploadet", + "ToastBatchUpdateFailed": "Mislykkedes batchopdatering", + "ToastBatchUpdateSuccess": "Batchopdatering lykkedes", + "ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke", + "ToastBookmarkCreateSuccess": "Bogmærke tilføjet", + "ToastBookmarkRemoveFailed": "Mislykkedes fjernelse af bogmærke", + "ToastBookmarkRemoveSuccess": "Bogmærke fjernet", + "ToastBookmarkUpdateFailed": "Mislykkedes opdatering af bogmærke", + "ToastBookmarkUpdateSuccess": "Bogmærke opdateret", + "ToastChaptersHaveErrors": "Kapitler har fejl", + "ToastChaptersMustHaveTitles": "Kapitler skal have titler", + "ToastCollectionItemsRemoveFailed": "Mislykkedes fjernelse af element(er) fra samlingen", + "ToastCollectionItemsRemoveSuccess": "Element(er) fjernet fra samlingen", + "ToastCollectionRemoveFailed": "Mislykkedes fjernelse af samling", + "ToastCollectionRemoveSuccess": "Samling fjernet", + "ToastCollectionUpdateFailed": "Mislykkedes opdatering af samling", + "ToastCollectionUpdateSuccess": "Samling opdateret", + "ToastItemCoverUpdateFailed": "Mislykkedes opdatering af varens omslag", + "ToastItemCoverUpdateSuccess": "Varens omslag opdateret", + "ToastItemDetailsUpdateFailed": "Mislykkedes opdatering af varedetaljer", + "ToastItemDetailsUpdateSuccess": "Varedetaljer opdateret", + "ToastItemDetailsUpdateUnneeded": "Ingen opdateringer er nødvendige for varedetaljer", + "ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet", + "ToastItemMarkedAsFinishedSuccess": "Vare markeret som afsluttet", + "ToastItemMarkedAsNotFinishedFailed": "Mislykkedes markering som ikke afsluttet", + "ToastItemMarkedAsNotFinishedSuccess": "Vare markeret som ikke afsluttet", + "ToastLibraryCreateFailed": "Mislykkedes oprettelse af bibliotek", + "ToastLibraryCreateSuccess": "Bibliotek \"{0}\" oprettet", + "ToastLibraryDeleteFailed": "Mislykkedes sletning af bibliotek", + "ToastLibraryDeleteSuccess": "Bibliotek slettet", + "ToastLibraryScanFailedToStart": "Mislykkedes start af skanning", + "ToastLibraryScanStarted": "Biblioteksskanning startet", + "ToastLibraryUpdateFailed": "Mislykkedes opdatering af bibliotek", + "ToastLibraryUpdateSuccess": "Bibliotek \"{0}\" opdateret", + "ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste", + "ToastPlaylistCreateSuccess": "Afspilningsliste oprettet", + "ToastPlaylistRemoveFailed": "Mislykkedes fjernelse af afspilningsliste", + "ToastPlaylistRemoveSuccess": "Afspilningsliste fjernet", + "ToastPlaylistUpdateFailed": "Mislykkedes opdatering af afspilningsliste", + "ToastPlaylistUpdateSuccess": "Afspilningsliste opdateret", + "ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast", + "ToastPodcastCreateSuccess": "Podcast oprettet med succes", + "ToastRemoveItemFromCollectionFailed": "Mislykkedes fjernelse af element fra samling", + "ToastRemoveItemFromCollectionSuccess": "Element fjernet fra samling", + "ToastRSSFeedCloseFailed": "Mislykkedes lukning af RSS-feed", + "ToastRSSFeedCloseSuccess": "RSS-feed lukket", + "ToastSendEbookToDeviceFailed": "Mislykkedes afsendelse af e-bog til enhed", + "ToastSendEbookToDeviceSuccess": "E-bog afsendt til enhed \"{0}\"", + "ToastSeriesUpdateFailed": "Mislykkedes opdatering af serie", + "ToastSeriesUpdateSuccess": "Serieopdatering lykkedes", + "ToastSessionDeleteFailed": "Mislykkedes sletning af session", + "ToastSessionDeleteSuccess": "Session slettet", + "ToastSocketConnected": "Socket forbundet", + "ToastSocketDisconnected": "Socket afbrudt", + "ToastSocketFailedToConnect": "Socket kunne ikke oprettes", + "ToastUserDeleteFailed": "Mislykkedes sletning af bruger", + "ToastUserDeleteSuccess": "Bruger slettet" +} \ No newline at end of file From c2643329945e1d6b853a22ac56e3ce3cbe5e16fb Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 15 Oct 2023 12:55:22 -0500 Subject: [PATCH 060/208] Fix:Scanner detecting library item folder renames #1161 --- server/scanner/LibraryItemScanner.js | 7 ++++--- server/scanner/LibraryScanner.js | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/server/scanner/LibraryItemScanner.js b/server/scanner/LibraryItemScanner.js index e9ca3302d..588b77444 100644 --- a/server/scanner/LibraryItemScanner.js +++ b/server/scanner/LibraryItemScanner.js @@ -21,9 +21,10 @@ class LibraryItemScanner { * Scan single library item * * @param {string} libraryItemId + * @param {{relPath:string, path:string}} [renamedPaths] used by watcher when item folder was renamed * @returns {number} ScanResult */ - async scanLibraryItem(libraryItemId) { + async scanLibraryItem(libraryItemId, renamedPaths = null) { // TODO: Add task manager const libraryItem = await Database.libraryItemModel.findByPk(libraryItemId) if (!libraryItem) { @@ -50,9 +51,9 @@ class LibraryItemScanner { const scanLogger = new ScanLogger() scanLogger.verbose = true - scanLogger.setData('libraryItem', libraryItem.relPath) + scanLogger.setData('libraryItem', renamedPaths?.relPath || libraryItem.relPath) - const libraryItemPath = fileUtils.filePathToPOSIX(libraryItem.path) + const libraryItemPath = renamedPaths?.path || fileUtils.filePathToPOSIX(libraryItem.path) const folder = library.libraryFolders[0] const libraryItemScanData = await this.getLibraryItemScanData(libraryItemPath, library, folder, false) diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index 64977e2d8..44ccdd05e 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -483,6 +483,7 @@ class LibraryScanner { path: potentialChildDirs }) + let renamedPaths = {} if (!existingLibraryItem) { const dirIno = await fileUtils.getIno(fullPath) existingLibraryItem = await Database.libraryItemModel.findOneOld({ @@ -493,6 +494,8 @@ class LibraryScanner { // Update library item paths for scan existingLibraryItem.path = fullPath existingLibraryItem.relPath = itemDir + renamedPaths.path = fullPath + renamedPaths.relPath = itemDir } } if (existingLibraryItem) { @@ -512,7 +515,7 @@ class LibraryScanner { // Scan library item for updates Logger.debug(`[LibraryScanner] Folder update for relative path "${itemDir}" is in library item "${existingLibraryItem.media.metadata.title}" - scan for updates`) - itemGroupingResults[itemDir] = await LibraryItemScanner.scanLibraryItem(existingLibraryItem.id) + itemGroupingResults[itemDir] = await LibraryItemScanner.scanLibraryItem(existingLibraryItem.id, renamedPaths) continue } else if (library.settings.audiobooksOnly && !fileUpdateGroup[itemDir].some?.(scanUtils.checkFilepathIsAudioFile)) { Logger.debug(`[LibraryScanner] Folder update for relative path "${itemDir}" has no audio files`) From 48a590df4a7765b03fde1e4fd78477181ea0913a Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 16 Oct 2023 17:08:50 -0500 Subject: [PATCH 061/208] Fix:Authors page description shows line breaks #2218 --- client/pages/author/_id.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/author/_id.vue b/client/pages/author/_id.vue index 61cfa715a..c9834d8d9 100644 --- a/client/pages/author/_id.vue +++ b/client/pages/author/_id.vue @@ -17,7 +17,7 @@ </div> <p v-if="author.description" class="text-white text-opacity-60 uppercase text-xs mb-2">{{ $strings.LabelDescription }}</p> - <p class="text-white max-w-3xl text-sm leading-5">{{ author.description }}</p> + <p class="text-white max-w-3xl text-sm leading-5 whitespace-pre-wrap">{{ author.description }}</p> </div> </div> From 0d5792405f54efa75ad098acf17de0113aad178e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 16 Oct 2023 17:47:44 -0500 Subject: [PATCH 062/208] Fix:Podcast episodes store RSS feed guid so they can be matched if the RSS feed changes the episode URL #2207 --- .../components/modals/podcast/EpisodeFeed.vue | 38 +++++++++---------- server/controllers/PodcastController.js | 7 ++-- server/managers/PodcastManager.js | 10 ++--- server/models/PodcastEpisode.js | 4 ++ server/objects/entities/PodcastEpisode.js | 5 +++ server/utils/podcastUtils.js | 34 +++++++++++------ 6 files changed, 59 insertions(+), 39 deletions(-) diff --git a/client/components/modals/podcast/EpisodeFeed.vue b/client/components/modals/podcast/EpisodeFeed.vue index 0f75644b1..1378dbe58 100644 --- a/client/components/modals/podcast/EpisodeFeed.vue +++ b/client/components/modals/podcast/EpisodeFeed.vue @@ -16,11 +16,11 @@ v-for="(episode, index) in episodesList" :key="index" class="relative" - :class="itemEpisodeMap[episode.cleanUrl] ? 'bg-primary bg-opacity-40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'" + :class="getIsEpisodeDownloaded(episode) ? 'bg-primary bg-opacity-40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'" @click="toggleSelectEpisode(episode)" > <div class="absolute top-0 left-0 h-full flex items-center p-2"> - <span v-if="itemEpisodeMap[episode.cleanUrl]" class="material-icons text-success text-xl">download_done</span> + <span v-if="getIsEpisodeDownloaded(episode)" class="material-icons text-success text-xl">download_done</span> <ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" /> </div> <div class="px-8 py-2"> @@ -93,7 +93,7 @@ export default { return this.libraryItem.media.metadata.title || 'Unknown' }, allDownloaded() { - return !this.episodesCleaned.some((episode) => !this.itemEpisodeMap[episode.cleanUrl]) + return !this.episodesCleaned.some((episode) => this.getIsEpisodeDownloaded(episode)) }, episodesSelected() { return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key]) @@ -104,18 +104,7 @@ export default { return this.$getString('LabelDownloadNEpisodes', [this.episodesSelected.length]) }, itemEpisodes() { - if (!this.libraryItem) return [] - return this.libraryItem.media.episodes || [] - }, - itemEpisodeMap() { - const map = {} - this.itemEpisodes.forEach((item) => { - if (item.enclosure) { - const cleanUrl = this.getCleanEpisodeUrl(item.enclosure.url) - map[cleanUrl] = true - } - }) - return map + return this.libraryItem?.media.episodes || [] }, episodesList() { return this.episodesCleaned.filter((episode) => { @@ -127,12 +116,23 @@ export default { if (this.episodesList.length === this.episodesCleaned.length) { return this.$strings.LabelSelectAllEpisodes } - const episodesNotDownloaded = this.episodesList.filter((ep) => !this.itemEpisodeMap[ep.cleanUrl]).length + const episodesNotDownloaded = this.episodesList.filter((ep) => !this.getIsEpisodeDownloaded(ep)).length return this.$getString('LabelSelectEpisodesShowing', [episodesNotDownloaded]) } }, methods: { + getIsEpisodeDownloaded(episode) { + return this.itemEpisodes.some((downloadedEpisode) => { + if (episode.guid && downloadedEpisode.guid === episode.guid) return true + if (!downloadedEpisode.enclosure?.url) return false + return this.getCleanEpisodeUrl(downloadedEpisode.enclosure.url) === episode.cleanUrl + }) + }, /** + * UPDATE: As of v2.4.5 guid is used for matching existing downloaded episodes if it is found on the RSS feed. + * Fallback to checking the clean url + * @see https://github.com/advplyr/audiobookshelf/issues/2207 + * * RSS feed episode url is used for matching with existing downloaded episodes. * Some RSS feeds include timestamps in the episode url (e.g. patreon) that can change on requests. * These need to be removed in order to detect the same episode each time the feed is pulled. @@ -169,13 +169,13 @@ export default { }, toggleSelectAll(val) { for (const episode of this.episodesList) { - if (this.itemEpisodeMap[episode.cleanUrl]) this.selectedEpisodes[episode.cleanUrl] = false + if (this.getIsEpisodeDownloaded(episode)) this.selectedEpisodes[episode.cleanUrl] = false else this.$set(this.selectedEpisodes, episode.cleanUrl, val) } }, checkSetIsSelectedAll() { for (const episode of this.episodesList) { - if (!this.itemEpisodeMap[episode.cleanUrl] && !this.selectedEpisodes[episode.cleanUrl]) { + if (!this.getIsEpisodeDownloaded(episode) && !this.selectedEpisodes[episode.cleanUrl]) { this.selectAll = false return } @@ -183,7 +183,7 @@ export default { this.selectAll = true }, toggleSelectEpisode(episode) { - if (this.itemEpisodeMap[episode.cleanUrl]) return + if (this.getIsEpisodeDownloaded(episode)) return this.$set(this.selectedEpisodes, episode.cleanUrl, !this.selectedEpisodes[episode.cleanUrl]) this.checkSetIsSelectedAll() }, diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index c4112db69..22c3cafa7 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -184,10 +184,9 @@ class PodcastController { Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user) return res.sendStatus(403) } - var libraryItem = req.libraryItem - - var episodes = req.body - if (!episodes || !episodes.length) { + const libraryItem = req.libraryItem + const episodes = req.body + if (!episodes?.length) { return res.sendStatus(400) } diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index 5dec21523..b88a38afd 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -201,7 +201,7 @@ class PodcastManager { }) // TODO: Should we check for open playback sessions for this episode? // TODO: remove all user progress for this episode - if (oldestEpisode && oldestEpisode.audioFile) { + if (oldestEpisode?.audioFile) { Logger.info(`[PodcastManager] Deleting oldest episode "${oldestEpisode.title}"`) const successfullyDeleted = await removeFile(oldestEpisode.audioFile.metadata.path) if (successfullyDeleted) { @@ -246,7 +246,7 @@ class PodcastManager { Logger.debug(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`) var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter, libraryItem.media.maxNewEpisodesToDownload) - Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes ? newEpisodes.length : 'N/A'} episodes found`) + Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes?.length || 'N/A'} episodes found`) if (!newEpisodes) { // Failed // Allow up to MaxFailedEpisodeChecks failed attempts before disabling auto download @@ -280,14 +280,14 @@ class PodcastManager { Logger.error(`[PodcastManager] checkPodcastForNewEpisodes no feed url for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`) return false } - var feed = await getPodcastFeed(podcastLibraryItem.media.metadata.feedUrl) - if (!feed || !feed.episodes) { + const feed = await getPodcastFeed(podcastLibraryItem.media.metadata.feedUrl) + if (!feed?.episodes) { Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`, feed) return false } // Filter new and not already has - var newEpisodes = feed.episodes.filter(ep => ep.publishedAt > dateToCheckForEpisodesAfter && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url)) + let newEpisodes = feed.episodes.filter(ep => ep.publishedAt > dateToCheckForEpisodesAfter && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url)) if (maxNewEpisodes > 0) { newEpisodes = newEpisodes.slice(0, maxNewEpisodes) diff --git a/server/models/PodcastEpisode.js b/server/models/PodcastEpisode.js index 6416627ac..55b2f9d40 100644 --- a/server/models/PodcastEpisode.js +++ b/server/models/PodcastEpisode.js @@ -79,6 +79,7 @@ class PodcastEpisode extends Model { subtitle: this.subtitle, description: this.description, enclosure, + guid: this.extraData?.guid || null, pubDate: this.pubDate, chapters: this.chapters, audioFile: this.audioFile, @@ -98,6 +99,9 @@ class PodcastEpisode extends Model { if (oldEpisode.oldEpisodeId) { extraData.oldEpisodeId = oldEpisode.oldEpisodeId } + if (oldEpisode.guid) { + extraData.guid = oldEpisode.guid + } return { id: oldEpisode.id, index: oldEpisode.index, diff --git a/server/objects/entities/PodcastEpisode.js b/server/objects/entities/PodcastEpisode.js index 2b91aeb68..0a8f33493 100644 --- a/server/objects/entities/PodcastEpisode.js +++ b/server/objects/entities/PodcastEpisode.js @@ -20,6 +20,7 @@ class PodcastEpisode { this.subtitle = null this.description = null this.enclosure = null + this.guid = null this.pubDate = null this.chapters = [] @@ -46,6 +47,7 @@ class PodcastEpisode { this.subtitle = episode.subtitle this.description = episode.description this.enclosure = episode.enclosure ? { ...episode.enclosure } : null + this.guid = episode.guid || null this.pubDate = episode.pubDate this.chapters = episode.chapters?.map(ch => ({ ...ch })) || [] this.audioFile = new AudioFile(episode.audioFile) @@ -70,6 +72,7 @@ class PodcastEpisode { subtitle: this.subtitle, description: this.description, enclosure: this.enclosure ? { ...this.enclosure } : null, + guid: this.guid, pubDate: this.pubDate, chapters: this.chapters.map(ch => ({ ...ch })), audioFile: this.audioFile.toJSON(), @@ -93,6 +96,7 @@ class PodcastEpisode { subtitle: this.subtitle, description: this.description, enclosure: this.enclosure ? { ...this.enclosure } : null, + guid: this.guid, pubDate: this.pubDate, chapters: this.chapters.map(ch => ({ ...ch })), audioFile: this.audioFile.toJSON(), @@ -133,6 +137,7 @@ class PodcastEpisode { this.pubDate = data.pubDate || '' this.description = data.description || '' this.enclosure = data.enclosure ? { ...data.enclosure } : null + this.guid = data.guid || null this.season = data.season || '' this.episode = data.episode || '' this.episodeType = data.episodeType || 'full' diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 2fd684ea9..0e68a0a42 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -4,7 +4,7 @@ const { xmlToJSON, levenshteinDistance } = require('./index') const htmlSanitizer = require('../utils/htmlSanitizer') function extractFirstArrayItem(json, key) { - if (!json[key] || !json[key].length) return null + if (!json[key]?.length) return null return json[key][0] } @@ -110,13 +110,24 @@ function extractEpisodeData(item) { const pubDate = extractFirstArrayItem(item, 'pubDate') if (typeof pubDate === 'string') { episode.pubDate = pubDate - } else if (pubDate && typeof pubDate._ === 'string') { + } else if (typeof pubDate?._ === 'string') { episode.pubDate = pubDate._ } else { Logger.error(`[podcastUtils] Invalid pubDate ${item['pubDate']} for ${episode.enclosure.url}`) } } + if (item['guid']) { + const guidItem = extractFirstArrayItem(item, 'guid') + if (typeof guidItem === 'string') { + episode.guid = guidItem + } else if (typeof guidItem?._ === 'string') { + episode.guid = guidItem._ + } else { + Logger.error(`[podcastUtils] Invalid guid ${item['guid']} for ${episode.enclosure.url}`) + } + } + const arrayFields = ['title', 'itunes:episodeType', 'itunes:season', 'itunes:episode', 'itunes:author', 'itunes:duration', 'itunes:explicit', 'itunes:subtitle'] arrayFields.forEach((key) => { const cleanKey = key.split(':').pop() @@ -142,6 +153,7 @@ function cleanEpisodeData(data) { explicit: data.explicit || '', publishedAt, enclosure: data.enclosure, + guid: data.guid || null, chaptersUrl: data.chaptersUrl || null, chaptersType: data.chaptersType || null } @@ -159,16 +171,16 @@ function extractPodcastEpisodes(items) { } function cleanPodcastJson(rssJson, excludeEpisodeMetadata) { - if (!rssJson.channel || !rssJson.channel.length) { + if (!rssJson.channel?.length) { Logger.error(`[podcastUtil] Invalid podcast no channel object`) return null } - var channel = rssJson.channel[0] - if (!channel.item || !channel.item.length) { + const channel = rssJson.channel[0] + if (!channel.item?.length) { Logger.error(`[podcastUtil] Invalid podcast no episodes`) return null } - var podcast = { + const podcast = { metadata: extractPodcastMetadata(channel) } if (!excludeEpisodeMetadata) { @@ -181,8 +193,8 @@ function cleanPodcastJson(rssJson, excludeEpisodeMetadata) { module.exports.parsePodcastRssFeedXml = async (xml, excludeEpisodeMetadata = false, includeRaw = false) => { if (!xml) return null - var json = await xmlToJSON(xml) - if (!json || !json.rss) { + const json = await xmlToJSON(xml) + if (!json?.rss) { Logger.error('[podcastUtils] Invalid XML or RSS feed') return null } @@ -215,12 +227,12 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { data.data = data.data.toString() } - if (!data || !data.data) { + if (!data?.data) { Logger.error(`[podcastUtils] getPodcastFeed: Invalid podcast feed request response (${feedUrl})`) return false } Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}" success - parsing xml`) - var payload = await this.parsePodcastRssFeedXml(data.data, excludeEpisodeMetadata) + const payload = await this.parsePodcastRssFeedXml(data.data, excludeEpisodeMetadata) if (!payload) { return false } @@ -246,7 +258,7 @@ module.exports.findMatchingEpisodes = async (feedUrl, searchTitle) => { module.exports.findMatchingEpisodesInFeed = (feed, searchTitle) => { searchTitle = searchTitle.toLowerCase().trim() - if (!feed || !feed.episodes) { + if (!feed?.episodes) { return null } From b4ce5342c0add31936d722127987eb706fa86d8f Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 17 Oct 2023 17:46:43 -0500 Subject: [PATCH 063/208] Add:Tools tab on library modal, api endpoint to remove all metadata files from library item folders --- .../components/modals/libraries/EditModal.vue | 12 +++- .../modals/libraries/LibraryTools.vue | 70 +++++++++++++++++++ server/controllers/LibraryController.js | 50 +++++++++++++ server/routers/ApiRouter.js | 1 + 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 client/components/modals/libraries/LibraryTools.vue diff --git a/client/components/modals/libraries/EditModal.vue b/client/components/modals/libraries/EditModal.vue index 1fd011cf7..09c0fc1d7 100644 --- a/client/components/modals/libraries/EditModal.vue +++ b/client/components/modals/libraries/EditModal.vue @@ -12,9 +12,9 @@ </div> <div class="px-2 md:px-4 w-full text-sm pt-2 md:pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh"> - <component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :processing.sync="processing" @update="updateLibrary" @close="show = false" /> + <component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :library-id="libraryId" :processing.sync="processing" @update="updateLibrary" @close="show = false" /> - <div class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10"> + <div v-show="selectedTab !== 'tools'" class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10"> <div class="flex justify-end"> <ui-btn @click="submit">{{ buttonText }}</ui-btn> </div> @@ -57,6 +57,9 @@ export default { mediaType() { return this.libraryCopy?.mediaType }, + libraryId() { + return this.library?.id + }, tabs() { return [ { @@ -78,6 +81,11 @@ export default { id: 'schedule', title: this.$strings.HeaderSchedule, component: 'modals-libraries-schedule-scan' + }, + { + id: 'tools', + title: this.$strings.HeaderTools, + component: 'modals-libraries-library-tools' } ].filter((tab) => { return tab.id !== 'scanner' || this.mediaType === 'book' diff --git a/client/components/modals/libraries/LibraryTools.vue b/client/components/modals/libraries/LibraryTools.vue new file mode 100644 index 000000000..d1e62dd4b --- /dev/null +++ b/client/components/modals/libraries/LibraryTools.vue @@ -0,0 +1,70 @@ +<template> + <div class="w-full h-full px-1 md:px-4 py-1 mb-4"> + <ui-btn class="mb-4" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json files in library item folders</ui-btn> + <ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs files in library item folders</ui-btn> + </div> +</template> + +<script> +export default { + props: { + library: { + type: Object, + default: () => null + }, + libraryId: String, + processing: Boolean + }, + data() { + return {} + }, + computed: { + librarySettings() { + return this.library.settings || {} + }, + mediaType() { + return this.library.mediaType + }, + isBookLibrary() { + return this.mediaType === 'book' + } + }, + methods: { + removeAllMetadataClick(ext) { + const payload = { + message: `Are you sure you want to remove all metadata.${ext} files in your library item folders?`, + persistent: true, + callback: (confirmed) => { + if (confirmed) { + this.removeAllMetadataInLibrary(ext) + } + }, + type: 'yesNo' + } + this.$store.commit('globals/setConfirmPrompt', payload) + }, + removeAllMetadataInLibrary(ext) { + this.$emit('update:processing', true) + this.$axios + .$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`) + .then((data) => { + if (!data.found) { + this.$toast.info(`No metadata.${ext} files were found in library`) + } else if (!data.removed) { + this.$toast.success(`No metadata.${ext} files removed`) + } else { + this.$toast.success(`Successfully removed ${data.removed} metadata.${ext} files`) + } + }) + .catch((error) => { + console.error('Failed to remove metadata files', error) + this.$toast.error('Failed to remove metadata files') + }) + .finally(() => { + this.$emit('update:processing', false) + }) + } + }, + mounted() {} +} +</script> \ No newline at end of file diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 2b76d6b30..9c593ff28 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -854,6 +854,56 @@ class LibraryController { res.send(opmlText) } + /** + * Remove all metadata.json or metadata.abs files in library item folders + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async removeAllMetadataFiles(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryController] Non-admin user attempted to remove all metadata files`, req.user) + return res.sendStatus(403) + } + + const fileExt = req.query.ext === 'abs' ? 'abs' : 'json' + const metadataFilename = `metadata.${fileExt}` + const libraryItemsWithMetadata = await Database.libraryItemModel.findAll({ + attributes: ['id', 'libraryFiles'], + where: [ + { + libraryId: req.library.id + }, + Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(libraryFiles) AND json_extract(json_each.value, "$.metadata.filename") = "${metadataFilename}")`), { + [Sequelize.Op.gte]: 1 + }) + ] + }) + if (!libraryItemsWithMetadata.length) { + Logger.info(`[LibraryController] No ${metadataFilename} files found to remove`) + return res.json({ + found: 0 + }) + } + + Logger.info(`[LibraryController] Found ${libraryItemsWithMetadata.length} ${metadataFilename} files to remove`) + + let numRemoved = 0 + for (const libraryItem of libraryItemsWithMetadata) { + const metadataFilepath = libraryItem.libraryFiles.find(lf => lf.metadata.filename === metadataFilename)?.metadata.path + if (!metadataFilepath) continue + Logger.debug(`[LibraryController] Removing file "${metadataFilepath}"`) + if ((await fileUtils.removeFile(metadataFilepath))) { + numRemoved++ + } + } + + res.json({ + found: libraryItemsWithMetadata.length, + removed: numRemoved + }) + } + /** * Middleware that is not using libraryItems from memory * @param {import('express').Request} req diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index a90d18733..03a0696c4 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -84,6 +84,7 @@ class ApiRouter { this.router.get('/libraries/:id/recent-episodes', LibraryController.middleware.bind(this), LibraryController.getRecentEpisodes.bind(this)) this.router.get('/libraries/:id/opml', LibraryController.middleware.bind(this), LibraryController.getOPMLFile.bind(this)) this.router.post('/libraries/order', LibraryController.reorder.bind(this)) + this.router.post('/libraries/:id/remove-metadata', LibraryController.middleware.bind(this), LibraryController.removeAllMetadataFiles.bind(this)) // // Item Routes From d22052c612dfb1e1ab0ebbccd97757d461af4287 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 18 Oct 2023 16:47:56 -0500 Subject: [PATCH 064/208] Update UI for library tools tab --- .../components/modals/libraries/EditModal.vue | 2 +- .../modals/libraries/LibraryTools.vue | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/components/modals/libraries/EditModal.vue b/client/components/modals/libraries/EditModal.vue index 09c0fc1d7..03b66931b 100644 --- a/client/components/modals/libraries/EditModal.vue +++ b/client/components/modals/libraries/EditModal.vue @@ -1,5 +1,5 @@ <template> - <modals-modal v-model="show" name="edit-library" :width="700" :height="'unset'" :processing="processing"> + <modals-modal v-model="show" name="edit-library" :width="800" :height="'unset'" :processing="processing"> <template #outer> <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden"> <p class="text-xl md:text-3xl text-white truncate">{{ title }}</p> diff --git a/client/components/modals/libraries/LibraryTools.vue b/client/components/modals/libraries/LibraryTools.vue index d1e62dd4b..7297c1aee 100644 --- a/client/components/modals/libraries/LibraryTools.vue +++ b/client/components/modals/libraries/LibraryTools.vue @@ -1,7 +1,18 @@ <template> - <div class="w-full h-full px-1 md:px-4 py-1 mb-4"> - <ui-btn class="mb-4" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json files in library item folders</ui-btn> - <ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs files in library item folders</ui-btn> + <div class="w-full h-full px-1 md:px-2 py-1 mb-4"> + <div class="w-full border border-black-200 p-4 my-8"> + <div class="flex flex-wrap items-center"> + <div> + <p class="text-lg">Remove metadata files in library item folders</p> + <p class="max-w-sm text-sm pt-2 text-gray-300">Remove all metadata.json or metadata.abs files in your {{ mediaType }} folders</p> + </div> + <div class="flex-grow" /> + <div> + <ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json</ui-btn> + <ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs</ui-btn> + </div> + </div> + </div> </div> </template> From 516b0b44642cc79005f6278bc2bd5078b3ee1f9a Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 18 Oct 2023 17:02:15 -0500 Subject: [PATCH 065/208] Fix:Book scanner set item as missing if no media files are found #2226 --- server/scanner/BookScanner.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index e579bcc95..f752417c1 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -339,6 +339,19 @@ class BookScanner { libraryItemUpdated = global.ServerSettings.storeMetadataWithItem && !existingLibraryItem.isFile } + // If book has no audio files and no ebook then it is considered missing + if (!media.audioFiles.length && !media.ebookFile) { + if (!existingLibraryItem.isMissing) { + libraryScan.addLog(LogLevel.INFO, `Book "${bookMetadata.title}" has no audio files and no ebook file. Setting library item as missing`) + existingLibraryItem.isMissing = true + libraryItemUpdated = true + } + } else if (existingLibraryItem.isMissing) { + libraryScan.addLog(LogLevel.INFO, `Book "${bookMetadata.title}" was missing but now has media files. Setting library item as NOT missing`) + existingLibraryItem.isMissing = false + libraryItemUpdated = true + } + // Check/update the isSupplementary flag on libraryFiles for the LibraryItem for (const libraryFile of existingLibraryItem.libraryFiles) { if (globals.SupportedEbookTypes.includes(libraryFile.metadata.ext.slice(1).toLowerCase())) { From 8c5ce6149f79b276991496606551339da82add69 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 18 Oct 2023 17:10:53 -0500 Subject: [PATCH 066/208] Fix:Aspect ratio of authors image on authors landing page #2227 --- client/components/app/StreamContainer.vue | 2 +- client/components/cards/AuthorCard.vue | 2 +- client/components/tables/collection/BookTableRow.vue | 2 +- client/components/tables/playlist/ItemTableRow.vue | 2 +- client/pages/author/_id.vue | 4 ++-- client/pages/item/_id/index.vue | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index d40ce0da5..1aecbf4ee 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -15,7 +15,7 @@ <div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div> <div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div> <div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base"> - <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}?library=${libraryId}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link> + <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link> </div> <div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div> <widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator> diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue index c06c53336..db4e7e9a7 100644 --- a/client/components/cards/AuthorCard.vue +++ b/client/components/cards/AuthorCard.vue @@ -1,5 +1,5 @@ <template> - <nuxt-link :to="`/author/${author.id}?library=${currentLibraryId}`"> + <nuxt-link :to="`/author/${author.id}`"> <div @mouseover="mouseover" @mouseleave="mouseleave"> <div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> <!-- Image or placeholder --> diff --git a/client/components/tables/collection/BookTableRow.vue b/client/components/tables/collection/BookTableRow.vue index 834088d9b..399c429a1 100644 --- a/client/components/tables/collection/BookTableRow.vue +++ b/client/components/tables/collection/BookTableRow.vue @@ -26,7 +26,7 @@ </div> <div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300"> <template v-for="(author, index) in bookAuthors"> - <nuxt-link :key="author.id" :to="`/author/${author.id}?library=${book.libraryId}`" class="truncate hover:underline">{{ author.name }}</nuxt-link + <nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link ><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span> </template> </div> diff --git a/client/components/tables/playlist/ItemTableRow.vue b/client/components/tables/playlist/ItemTableRow.vue index ff986a331..e5486461e 100644 --- a/client/components/tables/playlist/ItemTableRow.vue +++ b/client/components/tables/playlist/ItemTableRow.vue @@ -21,7 +21,7 @@ </div> <div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300"> <template v-for="(author, index) in bookAuthors"> - <nuxt-link :key="author.id" :to="`/author/${author.id}?library=${libraryItem.libraryId}`" class="truncate hover:underline">{{ author.name }}</nuxt-link + <nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link ><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span> </template> <nuxt-link v-if="episode" :to="`/item/${libraryItem.id}`" class="truncate hover:underline">{{ mediaMetadata.title }}</nuxt-link> diff --git a/client/pages/author/_id.vue b/client/pages/author/_id.vue index c9834d8d9..5ef1bef24 100644 --- a/client/pages/author/_id.vue +++ b/client/pages/author/_id.vue @@ -3,7 +3,7 @@ <div class="max-w-6xl mx-auto"> <div class="flex flex-wrap sm:flex-nowrap justify-center mb-6"> <div class="w-48 min-w-48"> - <div class="w-full h-52"> + <div class="w-full h-60"> <covers-author-image :author="author" rounded="0" /> </div> </div> @@ -44,7 +44,7 @@ <script> export default { async asyncData({ store, app, params, redirect, query }) { - const author = await app.$axios.$get(`/api/authors/${params.id}?library=${query.library || store.state.libraries.currentLibraryId}&include=items,series`).catch((error) => { + const author = await app.$axios.$get(`/api/authors/${params.id}?include=items,series`).catch((error) => { console.error('Failed to get author', error) return null }) diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 176725b91..43adc7274 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -42,7 +42,7 @@ <nuxt-link v-for="(artist, index) in musicArtists" :key="index" :to="`/artist/${$encode(artist)}`" class="hover:underline">{{ artist }}<span v-if="index < musicArtists.length - 1">, </span></nuxt-link> </p> <p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis"> - by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}?library=${libraryItem.libraryId}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link> + by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link> </p> <p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p> </template> From 22361d785d35e43adf517907c0812c377d7139b0 Mon Sep 17 00:00:00 2001 From: JBlond <leet31337@web.de> Date: Thu, 19 Oct 2023 15:22:00 +0200 Subject: [PATCH 067/208] Translate new string for DE language. --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index b72df02f0..50d0dbebb 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -266,7 +266,7 @@ "LabelHost": "Host", "LabelHour": "Stunde", "LabelIcon": "Symbol", - "LabelImageURLFromTheWeb": "Image URL from the web", + "LabelImageURLFromTheWeb": "Bild-URL vom Internet", "LabelIncludeInTracklist": "In die Titelliste aufnehmen", "LabelIncomplete": "Unvollständig", "LabelInProgress": "In Bearbeitung", From 4a5f534a658c17509ee4a9b9a8b8965986cdbccd Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 19 Oct 2023 16:30:33 -0500 Subject: [PATCH 068/208] Update:Description width of item page to match width of tables --- client/pages/item/_id/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 43adc7274..7e0bc3dd8 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -124,7 +124,7 @@ </ui-context-menu-dropdown> </div> - <div class="my-4 max-w-2xl"> + <div class="my-4 w-full"> <p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p> </div> From 920ddf43d721e9be823146e6154b89b36c550da8 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 19 Oct 2023 17:20:12 -0500 Subject: [PATCH 069/208] Remove unused old model functions --- server/Server.js | 1 - server/controllers2/libraryItem.controller.js | 16 -- server/db/libraryItem.db.js | 80 ------ server/objects/LibraryItem.js | 98 +------- server/objects/entities/PodcastEpisode.js | 92 +------ server/objects/mediaTypes/Book.js | 68 +---- server/objects/mediaTypes/Music.js | 19 -- server/objects/mediaTypes/Podcast.js | 51 +--- server/objects/mediaTypes/Video.js | 9 - server/objects/metadata/BookMetadata.js | 237 +----------------- server/objects/metadata/MusicMetadata.js | 15 +- server/objects/metadata/PodcastMetadata.js | 84 +------ server/objects/metadata/VideoMetadata.js | 13 - server/routers/ApiRouter.js | 1 - server/routes/index.js | 8 - server/routes/libraries.js | 7 - 16 files changed, 15 insertions(+), 784 deletions(-) delete mode 100644 server/controllers2/libraryItem.controller.js delete mode 100644 server/db/libraryItem.db.js delete mode 100644 server/routes/index.js delete mode 100644 server/routes/libraries.js diff --git a/server/Server.js b/server/Server.js index d1d36d0be..36780df4a 100644 --- a/server/Server.js +++ b/server/Server.js @@ -153,7 +153,6 @@ class Server { // Static folder router.use(express.static(Path.join(global.appRoot, 'static'))) - // router.use('/api/v1', routes) // TODO: New routes router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router) router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router) diff --git a/server/controllers2/libraryItem.controller.js b/server/controllers2/libraryItem.controller.js deleted file mode 100644 index 83b1776e2..000000000 --- a/server/controllers2/libraryItem.controller.js +++ /dev/null @@ -1,16 +0,0 @@ -const itemDb = require('../db/item.db') - -const getLibraryItem = async (req, res) => { - let libraryItem = null - if (req.query.expanded == 1) { - libraryItem = await itemDb.getLibraryItemExpanded(req.params.id) - } else { - libraryItem = await itemDb.getLibraryItemMinified(req.params.id) - } - - res.json(libraryItem) -} - -module.exports = { - getLibraryItem -} \ No newline at end of file diff --git a/server/db/libraryItem.db.js b/server/db/libraryItem.db.js deleted file mode 100644 index 335a52a17..000000000 --- a/server/db/libraryItem.db.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * TODO: Unused for testing - */ -const { Sequelize } = require('sequelize') -const Database = require('../Database') - -const getLibraryItemMinified = (libraryItemId) => { - return Database.libraryItemModel.findByPk(libraryItemId, { - include: [ - { - model: Database.bookModel, - attributes: [ - 'id', 'title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language', 'explicit', 'narrators', 'coverPath', 'genres', 'tags' - ], - include: [ - { - model: Database.authorModel, - attributes: ['id', 'name'], - through: { - attributes: [] - } - }, - { - model: Database.seriesModel, - attributes: ['id', 'name'], - through: { - attributes: ['sequence'] - } - } - ] - }, - { - model: Database.podcastModel, - attributes: [ - 'id', 'title', 'author', 'releaseDate', 'feedURL', 'imageURL', 'description', 'itunesPageURL', 'itunesId', 'itunesArtistId', 'language', 'podcastType', 'explicit', 'autoDownloadEpisodes', 'genres', 'tags', - [Sequelize.literal('(SELECT COUNT(*) FROM "podcastEpisodes" WHERE "podcastEpisodes"."podcastId" = podcast.id)'), 'numPodcastEpisodes'] - ] - } - ] - }) -} - -const getLibraryItemExpanded = (libraryItemId) => { - return Database.libraryItemModel.findByPk(libraryItemId, { - include: [ - { - model: Database.bookModel, - include: [ - { - model: Database.authorModel, - through: { - attributes: [] - } - }, - { - model: Database.seriesModel, - through: { - attributes: ['sequence'] - } - } - ] - }, - { - model: Database.podcastModel, - include: [ - { - model: Database.podcastEpisodeModel - } - ] - }, - 'libraryFolder', - 'library' - ] - }) -} - -module.exports = { - getLibraryItemMinified, - getLibraryItemExpanded -} \ No newline at end of file diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js index e36a5a925..bb91e2d6d 100644 --- a/server/objects/LibraryItem.js +++ b/server/objects/LibraryItem.js @@ -1,7 +1,6 @@ const uuidv4 = require("uuid").v4 const fs = require('../libs/fsExtra') const Path = require('path') -const { version } = require('../../package.json') const Logger = require('../Logger') const abmetadataGenerator = require('../utils/generators/abmetadataGenerator') const LibraryFile = require('./files/LibraryFile') @@ -9,7 +8,7 @@ const Book = require('./mediaTypes/Book') const Podcast = require('./mediaTypes/Podcast') const Video = require('./mediaTypes/Video') const Music = require('./mediaTypes/Music') -const { areEquivalent, copyValue, cleanStringForSearch } = require('../utils/index') +const { areEquivalent, copyValue } = require('../utils/index') const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') class LibraryItem { @@ -180,34 +179,23 @@ class LibraryItem { this.libraryFiles.forEach((lf) => total += lf.metadata.size) return total } - get audioFileTotalSize() { - let total = 0 - this.libraryFiles.filter(lf => lf.fileType == 'audio').forEach((lf) => total += lf.metadata.size) - return total - } get hasAudioFiles() { return this.libraryFiles.some(lf => lf.fileType === 'audio') } get hasMediaEntities() { return this.media.hasMediaEntities } - get hasIssues() { - if (this.isMissing || this.isInvalid) return true - return this.media.hasIssues - } // Data comes from scandir library item data + // TODO: Remove this function. Only used when creating a new podcast now setData(libraryMediaType, payload) { this.id = uuidv4() this.mediaType = libraryMediaType - if (libraryMediaType === 'video') { - this.media = new Video() - } else if (libraryMediaType === 'podcast') { + if (libraryMediaType === 'podcast') { this.media = new Podcast() - } else if (libraryMediaType === 'book') { - this.media = new Book() - } else if (libraryMediaType === 'music') { - this.media = new Music() + } else { + Logger.error(`[LibraryItem] setData called with unsupported media type "${libraryMediaType}"`) + return } this.media.id = uuidv4() this.media.libraryItemId = this.id @@ -270,85 +258,13 @@ class LibraryItem { this.updatedAt = Date.now() } - setInvalid() { - this.isInvalid = true - this.updatedAt = Date.now() - } - - setLastScan() { - this.lastScan = Date.now() - this.updatedAt = Date.now() - this.scanVersion = version - } - - // Returns null if file not found, true if file was updated, false if up to date - // updates existing LibraryFile, AudioFile, EBookFile's - checkFileFound(fileFound) { - let hasUpdated = false - - let existingFile = this.libraryFiles.find(lf => lf.ino === fileFound.ino) - let mediaFile = null - if (!existingFile) { - existingFile = this.libraryFiles.find(lf => lf.metadata.path === fileFound.metadata.path) - if (existingFile) { - // Update media file ino - mediaFile = this.media.findFileWithInode(existingFile.ino) - if (mediaFile) { - mediaFile.ino = fileFound.ino - } - - // file inode was updated - existingFile.ino = fileFound.ino - hasUpdated = true - } else { - // file not found - return null - } - } else { - mediaFile = this.media.findFileWithInode(existingFile.ino) - } - - if (existingFile.metadata.path !== fileFound.metadata.path) { - existingFile.metadata.path = fileFound.metadata.path - existingFile.metadata.relPath = fileFound.metadata.relPath - if (mediaFile) { - mediaFile.metadata.path = fileFound.metadata.path - mediaFile.metadata.relPath = fileFound.metadata.relPath - } - hasUpdated = true - } - - // FileMetadata keys - ['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size'].forEach((key) => { - if (existingFile.metadata[key] !== fileFound.metadata[key]) { - // Add modified flag on file data object if exists and was changed - if (key === 'mtimeMs' && existingFile.metadata[key]) { - fileFound.metadata.wasModified = true - } - - existingFile.metadata[key] = fileFound.metadata[key] - if (mediaFile) { - if (key === 'mtimeMs') mediaFile.metadata.wasModified = true - mediaFile.metadata[key] = fileFound.metadata[key] - } - hasUpdated = true - } - }) - - return hasUpdated - } - - searchQuery(query) { - query = cleanStringForSearch(query) - return this.media.searchQuery(query) - } - getDirectPlayTracklist(episodeId) { return this.media.getDirectPlayTracklist(episodeId) } /** * Save metadata.json/metadata.abs file + * TODO: Move to new LibraryItem model * @returns {Promise<LibraryFile>} null if not saved */ async saveMetadata() { diff --git a/server/objects/entities/PodcastEpisode.js b/server/objects/entities/PodcastEpisode.js index 0a8f33493..1452b7b59 100644 --- a/server/objects/entities/PodcastEpisode.js +++ b/server/objects/entities/PodcastEpisode.js @@ -1,7 +1,5 @@ const uuidv4 = require("uuid").v4 -const Path = require('path') -const Logger = require('../../Logger') -const { cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index') +const { areEquivalent, copyValue } = require('../../utils/index') const AudioFile = require('../files/AudioFile') const AudioTrack = require('../files/AudioTrack') @@ -146,19 +144,6 @@ class PodcastEpisode { this.updatedAt = Date.now() } - setDataFromAudioFile(audioFile, index) { - this.id = uuidv4() - this.audioFile = audioFile - this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename)) - this.index = index - - this.setDataFromAudioMetaTags(audioFile.metaTags, true) - - this.chapters = audioFile.chapters?.map((c) => ({ ...c })) - this.addedAt = Date.now() - this.updatedAt = Date.now() - } - update(payload) { let hasUpdates = false for (const key in this.toJSON()) { @@ -192,80 +177,5 @@ class PodcastEpisode { if (!this.enclosure || !this.enclosure.url) return false return this.enclosure.url == url } - - searchQuery(query) { - return cleanStringForSearch(this.title).includes(query) - } - - setDataFromAudioMetaTags(audioFileMetaTags, overrideExistingDetails = false) { - if (!audioFileMetaTags) return false - - const MetadataMapArray = [ - { - tag: 'tagComment', - altTag: 'tagSubtitle', - key: 'description' - }, - { - tag: 'tagSubtitle', - key: 'subtitle' - }, - { - tag: 'tagDate', - key: 'pubDate' - }, - { - tag: 'tagDisc', - key: 'season', - }, - { - tag: 'tagTrack', - altTag: 'tagSeriesPart', - key: 'episode' - }, - { - tag: 'tagTitle', - key: 'title' - }, - { - tag: 'tagEpisodeType', - key: 'episodeType' - } - ] - - MetadataMapArray.forEach((mapping) => { - let value = audioFileMetaTags[mapping.tag] - let tagToUse = mapping.tag - if (!value && mapping.altTag) { - tagToUse = mapping.altTag - value = audioFileMetaTags[mapping.altTag] - } - - if (value && typeof value === 'string') { - value = value.trim() // Trim whitespace - - if (mapping.key === 'pubDate' && (!this.pubDate || overrideExistingDetails)) { - const pubJsDate = new Date(value) - if (pubJsDate && !isNaN(pubJsDate)) { - this.publishedAt = pubJsDate.valueOf() - this.pubDate = value - Logger.debug(`[PodcastEpisode] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${this[mapping.key]}`) - } else { - Logger.warn(`[PodcastEpisode] Mapping pubDate with tag ${tagToUse} has invalid date "${value}"`) - } - } else if (mapping.key === 'episodeType' && (!this.episodeType || overrideExistingDetails)) { - if (['full', 'trailer', 'bonus'].includes(value)) { - this.episodeType = value - Logger.debug(`[PodcastEpisode] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${this[mapping.key]}`) - } else { - Logger.warn(`[PodcastEpisode] Mapping episodeType with invalid value "${value}". Must be one of [full, trailer, bonus].`) - } - } else if (!this[mapping.key] || overrideExistingDetails) { - this[mapping.key] = value - Logger.debug(`[PodcastEpisode] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${this[mapping.key]}`) - } - } - }) - } } module.exports = PodcastEpisode diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js index 33cbc016f..afbf1622a 100644 --- a/server/objects/mediaTypes/Book.js +++ b/server/objects/mediaTypes/Book.js @@ -1,9 +1,7 @@ const Logger = require('../../Logger') const BookMetadata = require('../metadata/BookMetadata') -const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index') -const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata') -const abmetadataGenerator = require('../../utils/generators/abmetadataGenerator') -const { readTextFile, filePathToPOSIX } = require('../../utils/fileUtils') +const { areEquivalent, copyValue } = require('../../utils/index') +const { filePathToPOSIX } = require('../../utils/fileUtils') const AudioFile = require('../files/AudioFile') const AudioTrack = require('../files/AudioTrack') const EBookFile = require('../files/EBookFile') @@ -111,23 +109,12 @@ class Book { get hasMediaEntities() { return !!this.tracks.length || this.ebookFile } - get shouldSearchForCover() { - if (this.coverPath) return false - if (!this.lastCoverSearch || this.metadata.coverSearchQuery !== this.lastCoverSearchQuery) return true - return (Date.now() - this.lastCoverSearch) > 1000 * 60 * 60 * 24 * 7 // 7 day - } - get hasEmbeddedCoverArt() { - return this.audioFiles.some(af => af.embeddedCoverArt) - } get invalidAudioFiles() { return this.audioFiles.filter(af => af.invalid) } get includedAudioFiles() { return this.audioFiles.filter(af => !af.exclude && !af.invalid) } - get hasIssues() { - return this.missingParts.length || this.invalidAudioFiles.length - } get tracks() { let startOffset = 0 return this.includedAudioFiles.map((af) => { @@ -226,57 +213,6 @@ class Book { return null } - updateLastCoverSearch(coverWasFound) { - this.lastCoverSearch = coverWasFound ? null : Date.now() - this.lastCoverSearchQuery = coverWasFound ? null : this.metadata.coverSearchQuery - } - - // Audio file metadata tags map to book details (will not overwrite) - setMetadataFromAudioFile(overrideExistingDetails = false) { - if (!this.audioFiles.length) return false - var audioFile = this.audioFiles[0] - if (!audioFile.metaTags) return false - return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails) - } - - setData(mediaPayload) { - this.metadata = new BookMetadata() - if (mediaPayload.metadata) { - this.metadata.setData(mediaPayload.metadata) - } - } - - searchQuery(query) { - const payload = { - tags: this.tags.filter(t => cleanStringForSearch(t).includes(query)), - series: this.metadata.searchSeries(query), - authors: this.metadata.searchAuthors(query), - narrators: this.metadata.searchNarrators(query), - matchKey: null, - matchText: null - } - const metadataMatch = this.metadata.searchQuery(query) - if (metadataMatch) { - payload.matchKey = metadataMatch.matchKey - payload.matchText = metadataMatch.matchText - } else { - if (payload.authors.length) { - payload.matchKey = 'authors' - payload.matchText = this.metadata.authorName - } else if (payload.series.length) { - payload.matchKey = 'series' - payload.matchText = this.metadata.seriesName - } else if (payload.tags.length) { - payload.matchKey = 'tags' - payload.matchText = this.tags.join(', ') - } else if (payload.narrators.length) { - payload.matchKey = 'narrators' - payload.matchText = this.metadata.narratorName - } - } - return payload - } - /** * Set the EBookFile from a LibraryFile * If null then ebookFile will be removed from the book diff --git a/server/objects/mediaTypes/Music.js b/server/objects/mediaTypes/Music.js index 7512c4ec7..d4b8a518a 100644 --- a/server/objects/mediaTypes/Music.js +++ b/server/objects/mediaTypes/Music.js @@ -65,15 +65,6 @@ class Music { get hasMediaEntities() { return !!this.audioFile } - get shouldSearchForCover() { - return false - } - get hasEmbeddedCoverArt() { - return this.audioFile.embeddedCoverArt - } - get hasIssues() { - return false - } get duration() { return this.audioFile.duration || 0 } @@ -134,16 +125,6 @@ class Music { this.audioFile = audioFile } - setMetadataFromAudioFile(overrideExistingDetails = false) { - if (!this.audioFile) return false - if (!this.audioFile.metaTags) return false - return this.metadata.setDataFromAudioMetaTags(this.audioFile.metaTags, overrideExistingDetails) - } - - searchQuery(query) { - return {} - } - // Only checks container format checkCanDirectPlay(payload) { return true diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js index 9ddb34129..969e25485 100644 --- a/server/objects/mediaTypes/Podcast.js +++ b/server/objects/mediaTypes/Podcast.js @@ -1,9 +1,8 @@ const Logger = require('../../Logger') const PodcastEpisode = require('../entities/PodcastEpisode') const PodcastMetadata = require('../metadata/PodcastMetadata') -const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index') -const abmetadataGenerator = require('../../utils/generators/abmetadataGenerator') -const { readTextFile, filePathToPOSIX } = require('../../utils/fileUtils') +const { areEquivalent, copyValue } = require('../../utils/index') +const { filePathToPOSIX } = require('../../utils/fileUtils') class Podcast { constructor(podcast) { @@ -110,15 +109,6 @@ class Podcast { get hasMediaEntities() { return !!this.episodes.length } - get shouldSearchForCover() { - return false - } - get hasEmbeddedCoverArt() { - return this.episodes.some(ep => ep.audioFile.embeddedCoverArt) - } - get hasIssues() { - return false - } get duration() { let total = 0 this.episodes.forEach((ep) => total += ep.duration) @@ -187,10 +177,6 @@ class Podcast { return null } - findEpisodeWithInode(inode) { - return this.episodes.find(ep => ep.audioFile.ino === inode) - } - setData(mediaData) { this.metadata = new PodcastMetadata() if (mediaData.metadata) { @@ -203,31 +189,6 @@ class Podcast { this.lastEpisodeCheck = Date.now() // Makes sure new episodes are after this } - searchEpisodes(query) { - return this.episodes.filter(ep => ep.searchQuery(query)) - } - - searchQuery(query) { - const payload = { - tags: this.tags.filter(t => cleanStringForSearch(t).includes(query)), - matchKey: null, - matchText: null - } - const metadataMatch = this.metadata.searchQuery(query) - if (metadataMatch) { - payload.matchKey = metadataMatch.matchKey - payload.matchText = metadataMatch.matchText - } else { - const matchingEpisodes = this.searchEpisodes(query) - if (matchingEpisodes.length) { - payload.matchKey = 'episode' - payload.matchText = matchingEpisodes[0].title - } - } - - return payload - } - checkHasEpisode(episodeId) { return this.episodes.some(ep => ep.id === episodeId) } @@ -294,14 +255,6 @@ class Podcast { return this.episodes.find(ep => ep.id == episodeId) } - // Audio file metadata tags map to podcast details - setMetadataFromAudioFile(overrideExistingDetails = false) { - if (!this.episodes.length) return false - const audioFile = this.episodes[0].audioFile - if (!audioFile?.metaTags) return false - return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails) - } - getChapters(episodeId) { return this.getEpisode(episodeId)?.chapters?.map(ch => ({ ...ch })) || [] } diff --git a/server/objects/mediaTypes/Video.js b/server/objects/mediaTypes/Video.js index dae834c1f..940eab0bb 100644 --- a/server/objects/mediaTypes/Video.js +++ b/server/objects/mediaTypes/Video.js @@ -69,15 +69,6 @@ class Video { get hasMediaEntities() { return true } - get shouldSearchForCover() { - return false - } - get hasEmbeddedCoverArt() { - return false - } - get hasIssues() { - return false - } get duration() { return 0 } diff --git a/server/objects/metadata/BookMetadata.js b/server/objects/metadata/BookMetadata.js index 9fb07bc8b..490b99493 100644 --- a/server/objects/metadata/BookMetadata.js +++ b/server/objects/metadata/BookMetadata.js @@ -1,5 +1,5 @@ const Logger = require('../../Logger') -const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index') +const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index') const parseNameString = require('../../utils/parsers/parseNameString') class BookMetadata { constructor(metadata) { @@ -144,20 +144,6 @@ class BookMetadata { return `${se.name} #${se.sequence}` }).join(', ') } - get seriesNameIgnorePrefix() { - if (!this.series.length) return '' - return this.series.map(se => { - if (!se.sequence) return getTitleIgnorePrefix(se.name) - return `${getTitleIgnorePrefix(se.name)} #${se.sequence}` - }).join(', ') - } - get seriesNamePrefixAtEnd() { - if (!this.series.length) return '' - return this.series.map(se => { - if (!se.sequence) return getTitlePrefixAtEnd(se.name) - return `${getTitlePrefixAtEnd(se.name)} #${se.sequence}` - }).join(', ') - } get firstSeriesName() { if (!this.series.length) return '' return this.series[0].name @@ -169,36 +155,15 @@ class BookMetadata { get narratorName() { return this.narrators.join(', ') } - get coverSearchQuery() { - if (!this.authorName) return this.title - return this.title + '&' + this.authorName - } - hasAuthor(id) { - return !!this.authors.find(au => au.id == id) - } - hasSeries(seriesId) { - return !!this.series.find(se => se.id == seriesId) - } - hasNarrator(narratorName) { - return this.narrators.includes(narratorName) - } getSeries(seriesId) { return this.series.find(se => se.id == seriesId) } - getFirstSeries() { - return this.series.length ? this.series[0] : null - } getSeriesSequence(seriesId) { const series = this.series.find(se => se.id == seriesId) if (!series) return null return series.sequence || '' } - getSeriesSortTitle(series) { - if (!series) return '' - if (!series.sequence) return series.name - return `${series.name} #${series.sequence}` - } update(payload) { const json = this.toJSON() @@ -231,205 +196,5 @@ class BookMetadata { name: newAuthor.name }) } - - /** - * Update narrator name if narrator is in book - * @param {String} oldNarratorName - Narrator name to get updated - * @param {String} newNarratorName - Updated narrator name - * @return {Boolean} True if narrator was updated - */ - updateNarrator(oldNarratorName, newNarratorName) { - if (!this.hasNarrator(oldNarratorName)) return false - this.narrators = this.narrators.filter(n => n !== oldNarratorName) - if (newNarratorName && !this.hasNarrator(newNarratorName)) { - this.narrators.push(newNarratorName) - } - return true - } - - /** - * Remove narrator name if narrator is in book - * @param {String} narratorName - Narrator name to remove - * @return {Boolean} True if narrator was updated - */ - removeNarrator(narratorName) { - if (!this.hasNarrator(narratorName)) return false - this.narrators = this.narrators.filter(n => n !== narratorName) - return true - } - - setData(scanMediaData = {}) { - this.title = scanMediaData.title || null - this.subtitle = scanMediaData.subtitle || null - this.narrators = this.parseNarratorsTag(scanMediaData.narrators) - this.publishedYear = scanMediaData.publishedYear || null - this.description = scanMediaData.description || null - this.isbn = scanMediaData.isbn || null - this.asin = scanMediaData.asin || null - this.language = scanMediaData.language || null - this.genres = [] - this.explicit = !!scanMediaData.explicit - - if (scanMediaData.author) { - this.authors = this.parseAuthorsTag(scanMediaData.author) - } - if (scanMediaData.series) { - this.series = this.parseSeriesTag(scanMediaData.series, scanMediaData.sequence) - } - } - - setDataFromAudioMetaTags(audioFileMetaTags, overrideExistingDetails = false) { - const MetadataMapArray = [ - { - tag: 'tagComposer', - key: 'narrators' - }, - { - tag: 'tagDescription', - altTag: 'tagComment', - key: 'description' - }, - { - tag: 'tagPublisher', - key: 'publisher' - }, - { - tag: 'tagDate', - key: 'publishedYear' - }, - { - tag: 'tagSubtitle', - key: 'subtitle' - }, - { - tag: 'tagAlbum', - altTag: 'tagTitle', - key: 'title', - }, - { - tag: 'tagArtist', - altTag: 'tagAlbumArtist', - key: 'authors' - }, - { - tag: 'tagGenre', - key: 'genres' - }, - { - tag: 'tagSeries', - key: 'series' - }, - { - tag: 'tagIsbn', - key: 'isbn' - }, - { - tag: 'tagLanguage', - key: 'language' - }, - { - tag: 'tagASIN', - key: 'asin' - } - ] - - const updatePayload = {} - - // Metadata is only mapped to the book if it is empty - MetadataMapArray.forEach((mapping) => { - let value = audioFileMetaTags[mapping.tag] - // let tagToUse = mapping.tag - if (!value && mapping.altTag) { - value = audioFileMetaTags[mapping.altTag] - // tagToUse = mapping.altTag - } - - if (value && typeof value === 'string') { - value = value.trim() // Trim whitespace - - if (mapping.key === 'narrators' && (!this.narrators.length || overrideExistingDetails)) { - updatePayload.narrators = this.parseNarratorsTag(value) - } else if (mapping.key === 'authors' && (!this.authors.length || overrideExistingDetails)) { - updatePayload.authors = this.parseAuthorsTag(value) - } else if (mapping.key === 'genres' && (!this.genres.length || overrideExistingDetails)) { - updatePayload.genres = this.parseGenresTag(value) - } else if (mapping.key === 'series' && (!this.series.length || overrideExistingDetails)) { - const sequenceTag = audioFileMetaTags.tagSeriesPart || null - updatePayload.series = this.parseSeriesTag(value, sequenceTag) - } else if (!this[mapping.key] || overrideExistingDetails) { - updatePayload[mapping.key] = value - // Logger.debug(`[Book] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key]}`) - } - } - }) - - if (Object.keys(updatePayload).length) { - return this.update(updatePayload) - } - return false - } - - // Returns array of names in First Last format - parseNarratorsTag(narratorsTag) { - const parsed = parseNameString.parse(narratorsTag) - return parsed ? parsed.names : [] - } - - // Return array of authors minified with placeholder id - parseAuthorsTag(authorsTag) { - const parsed = parseNameString.parse(authorsTag) - if (!parsed) return [] - return (parsed.names || []).map((au) => { - const findAuthor = this.authors.find(_au => _au.name == au) - - return { - id: findAuthor?.id || `new-${Math.floor(Math.random() * 1000000)}`, - name: au - } - }) - } - - parseGenresTag(genreTag) { - if (!genreTag || !genreTag.length) return [] - const separators = ['/', '//', ';'] - for (let i = 0; i < separators.length; i++) { - if (genreTag.includes(separators[i])) { - return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g) - } - } - return [genreTag] - } - - // Return array with series with placeholder id - parseSeriesTag(seriesTag, sequenceTag) { - if (!seriesTag) return [] - return [{ - id: `new-${Math.floor(Math.random() * 1000000)}`, - name: seriesTag, - sequence: sequenceTag || '' - }] - } - - searchSeries(query) { - return this.series.filter(se => cleanStringForSearch(se.name).includes(query)) - } - searchAuthors(query) { - return this.authors.filter(au => cleanStringForSearch(au.name).includes(query)) - } - searchNarrators(query) { - return this.narrators.filter(n => cleanStringForSearch(n).includes(query)) - } - searchQuery(query) { // Returns key if match is found - const keysToCheck = ['title', 'asin', 'isbn', 'subtitle'] - for (const key of keysToCheck) { - if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) { - return { - matchKey: key, - matchText: this[key] - } - } - } - return null - } } module.exports = BookMetadata diff --git a/server/objects/metadata/MusicMetadata.js b/server/objects/metadata/MusicMetadata.js index 7da473142..90a887e07 100644 --- a/server/objects/metadata/MusicMetadata.js +++ b/server/objects/metadata/MusicMetadata.js @@ -1,5 +1,5 @@ const Logger = require('../../Logger') -const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index') +const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index') class MusicMetadata { constructor(metadata) { @@ -133,19 +133,6 @@ class MusicMetadata { return getTitlePrefixAtEnd(this.title) } - searchQuery(query) { // Returns key if match is found - const keysToCheck = ['title', 'album'] - for (const key of keysToCheck) { - if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) { - return { - matchKey: key, - matchText: this[key] - } - } - } - return null - } - setData(mediaMetadata = {}) { this.title = mediaMetadata.title || null this.artist = mediaMetadata.artist || null diff --git a/server/objects/metadata/PodcastMetadata.js b/server/objects/metadata/PodcastMetadata.js index 2c371c6cb..8300e93a6 100644 --- a/server/objects/metadata/PodcastMetadata.js +++ b/server/objects/metadata/PodcastMetadata.js @@ -1,5 +1,5 @@ const Logger = require('../../Logger') -const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index') +const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index') class PodcastMetadata { constructor(metadata) { @@ -91,19 +91,6 @@ class PodcastMetadata { return getTitlePrefixAtEnd(this.title) } - searchQuery(query) { // Returns key if match is found - const keysToCheck = ['title', 'author', 'itunesId', 'itunesArtistId'] - for (const key of keysToCheck) { - if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) { - return { - matchKey: key, - matchText: this[key] - } - } - } - return null - } - setData(mediaMetadata = {}) { this.title = mediaMetadata.title || null this.author = mediaMetadata.author || null @@ -136,74 +123,5 @@ class PodcastMetadata { } return hasUpdates } - - setDataFromAudioMetaTags(audioFileMetaTags, overrideExistingDetails = false) { - const MetadataMapArray = [ - { - tag: 'tagAlbum', - altTag: 'tagSeries', - key: 'title' - }, - { - tag: 'tagArtist', - key: 'author' - }, - { - tag: 'tagGenre', - key: 'genres' - }, - { - tag: 'tagLanguage', - key: 'language' - }, - { - tag: 'tagItunesId', - key: 'itunesId' - }, - { - tag: 'tagPodcastType', - key: 'type', - } - ] - - const updatePayload = {} - - MetadataMapArray.forEach((mapping) => { - let value = audioFileMetaTags[mapping.tag] - let tagToUse = mapping.tag - if (!value && mapping.altTag) { - value = audioFileMetaTags[mapping.altTag] - tagToUse = mapping.altTag - } - - if (value && typeof value === 'string') { - value = value.trim() // Trim whitespace - - if (mapping.key === 'genres' && (!this.genres.length || overrideExistingDetails)) { - updatePayload.genres = this.parseGenresTag(value) - Logger.debug(`[Podcast] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload.genres.join(', ')}`) - } else if (!this[mapping.key] || overrideExistingDetails) { - updatePayload[mapping.key] = value - Logger.debug(`[Podcast] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key]}`) - } - } - }) - - if (Object.keys(updatePayload).length) { - return this.update(updatePayload) - } - return false - } - - parseGenresTag(genreTag) { - if (!genreTag || !genreTag.length) return [] - const separators = ['/', '//', ';'] - for (let i = 0; i < separators.length; i++) { - if (genreTag.includes(separators[i])) { - return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g) - } - } - return [genreTag] - } } module.exports = PodcastMetadata diff --git a/server/objects/metadata/VideoMetadata.js b/server/objects/metadata/VideoMetadata.js index 15d57fbe2..a2194d15e 100644 --- a/server/objects/metadata/VideoMetadata.js +++ b/server/objects/metadata/VideoMetadata.js @@ -55,19 +55,6 @@ class VideoMetadata { return getTitlePrefixAtEnd(this.title) } - searchQuery(query) { // Returns key if match is found - var keysToCheck = ['title'] - for (var key of keysToCheck) { - if (this[key] && String(this[key]).toLowerCase().includes(query)) { - return { - matchKey: key, - matchText: this[key] - } - } - } - return null - } - setData(mediaMetadata = {}) { this.title = mediaMetadata.title || null this.description = mediaMetadata.description || null diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 03a0696c4..034951df5 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -1,4 +1,3 @@ -const sequelize = require('sequelize') const express = require('express') const Path = require('path') diff --git a/server/routes/index.js b/server/routes/index.js deleted file mode 100644 index e638a9c5c..000000000 --- a/server/routes/index.js +++ /dev/null @@ -1,8 +0,0 @@ -const express = require('express') -const libraries = require('./libraries') - -const router = express.Router() - -router.use('/libraries', libraries) - -module.exports = router \ No newline at end of file diff --git a/server/routes/libraries.js b/server/routes/libraries.js deleted file mode 100644 index 07ab5ccd5..000000000 --- a/server/routes/libraries.js +++ /dev/null @@ -1,7 +0,0 @@ -const express = require('express') - -const router = express.Router() - -// TODO: Add library routes - -module.exports = router \ No newline at end of file From 5644a40a031879e39c35ff324bca8a76e02e03c7 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 20 Oct 2023 16:08:57 -0500 Subject: [PATCH 070/208] Update:Add missing tranlations #2217 --- client/components/app/Appbar.vue | 8 ++++---- client/components/cards/LazyBookCard.vue | 4 ++-- client/pages/item/_id/index.vue | 4 ++-- client/strings/da.json | 5 +++++ client/strings/de.json | 5 +++++ client/strings/en-us.json | 5 +++++ client/strings/es.json | 5 +++++ client/strings/fr.json | 5 +++++ client/strings/gu.json | 5 +++++ client/strings/hi.json | 5 +++++ client/strings/hr.json | 5 +++++ client/strings/it.json | 5 +++++ client/strings/lt.json | 5 +++++ client/strings/nl.json | 5 +++++ client/strings/no.json | 5 +++++ client/strings/pl.json | 5 +++++ client/strings/ru.json | 5 +++++ client/strings/zh-cn.json | 5 +++++ 18 files changed, 83 insertions(+), 8 deletions(-) diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index 879d50a32..92599c7aa 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -186,7 +186,7 @@ export default { methods: { requestBatchQuickEmbed() { const payload = { - message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?', + message: this.$strings.MessageConfirmQuickEmbed, callback: (confirmed) => { if (confirmed) { this.$axios @@ -219,7 +219,7 @@ export default { }, async batchRescan() { const payload = { - message: `Are you sure you want to re-scan ${this.selectedMediaItems.length} items?`, + message: this.$getString('MessageConfirmReScanLibraryItems', [this.selectedMediaItems.length]), callback: (confirmed) => { if (confirmed) { this.$axios @@ -316,8 +316,8 @@ export default { }, batchDeleteClick() { const payload = { - message: `This will delete ${this.numMediaItemsSelected} library items from the database and your file system. Are you sure?`, - checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', + message: this.$getString('MessageConfirmDeleteLibraryItems', [this.numMediaItemsSelected]), + checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox, yesButtonText: this.$strings.ButtonDelete, yesButtonColor: 'error', checkboxDefaultValue: true, diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index d51ed2085..d3f956ec6 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -843,8 +843,8 @@ export default { }, deleteLibraryItem() { const payload = { - message: 'This will delete the library item from the database and your file system. Are you sure?', - checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', + message: this.$strings.MessageConfirmDeleteLibraryItem, + checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox, yesButtonText: this.$strings.ButtonDelete, yesButtonColor: 'error', checkboxDefaultValue: true, diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 7e0bc3dd8..0f4f17b24 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -682,8 +682,8 @@ export default { }, deleteLibraryItem() { const payload = { - message: 'This will delete the library item from the database and your file system. Are you sure?', - checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', + message: this.$strings.MessageConfirmDeleteLibraryItem, + checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox, yesButtonText: this.$strings.ButtonDelete, yesButtonColor: 'error', checkboxDefaultValue: true, diff --git a/client/strings/da.json b/client/strings/da.json index 359ecdd62..905beb262 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -218,6 +218,7 @@ "LabelCurrently": "Aktuelt:", "LabelCustomCronExpression": "Brugerdefineret Cron Udtryk:", "LabelDatetime": "Dato og Tid", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Beskrivelse", "LabelDeselectAll": "Fravælg Alle", "LabelDevice": "Enheds", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Er du sikker på, at du vil slette backup for {0}?", "MessageConfirmDeleteFile": "Dette vil slette filen fra dit filsystem. Er du sikker?", "MessageConfirmDeleteLibrary": "Er du sikker på, at du vil slette biblioteket permanent \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Er du sikker på, at du vil slette denne session?", "MessageConfirmForceReScan": "Er du sikker på, at du vil tvinge en genindlæsning?", "MessageConfirmMarkAllEpisodesFinished": "Er du sikker på, at du vil markere alle episoder som afsluttet?", "MessageConfirmMarkAllEpisodesNotFinished": "Er du sikker på, at du vil markere alle episoder som ikke afsluttet?", "MessageConfirmMarkSeriesFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som afsluttet?", "MessageConfirmMarkSeriesNotFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som ikke afsluttet?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Er du sikker på, at du vil fjerne alle kapitler?", "MessageConfirmRemoveAuthor": "Er du sikker på, at du vil fjerne forfatteren \"{0}\"?", "MessageConfirmRemoveCollection": "Er du sikker på, at du vil fjerne samlingen \"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Er du sikker på, at du vil omdøbe tag \"{0}\" til \"{1}\" for alle elementer?", "MessageConfirmRenameTagMergeNote": "Bemærk: Dette tag findes allerede, så de vil blive fusioneret.", "MessageConfirmRenameTagWarning": "Advarsel! Et lignende tag med en anden skrivemåde eksisterer allerede \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Er du sikker på, at du vil sende {0} e-bog \"{1}\" til enhed \"{2}\"?", "MessageDownloadingEpisode": "Downloader episode", "MessageDragFilesIntoTrackOrder": "Træk filer ind i korrekt spororden", diff --git a/client/strings/de.json b/client/strings/de.json index 50d0dbebb..7bcc2df99 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -218,6 +218,7 @@ "LabelCurrently": "Aktuell:", "LabelCustomCronExpression": "Benutzerdefinierter Cron-Ausdruck", "LabelDatetime": "Datum & Uhrzeit", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Beschreibung", "LabelDeselectAll": "Alles abwählen", "LabelDevice": "Gerät", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?", "MessageConfirmDeleteFile": "Es wird die Datei vom System löschen. Sind Sie sicher?", "MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?", "MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?", "MessageConfirmMarkAllEpisodesFinished": "Sind Sie sicher, dass Sie alle Episoden als abgeschlossen markieren möchten?", "MessageConfirmMarkAllEpisodesNotFinished": "Sind Sie sicher, dass Sie alle Episoden als nicht abgeschlossen markieren möchten?", "MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?", "MessageConfirmMarkSeriesNotFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als nicht abgeschlossen markieren wollen?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Sind Sie sicher, dass Sie alle Kapitel entfernen möchten?", "MessageConfirmRemoveAuthor": "Sind Sie sicher, dass Sie den Autor \"{0}\" enfernen möchten?", "MessageConfirmRemoveCollection": "Sind Sie sicher, dass Sie die Sammlung \"{0}\" löschen wollen?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Sind Sie sicher, dass Sie den Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?", "MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.", "MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Sind Sie sicher, dass sie {0} ebook \"{1}\" auf das Gerät \"{2}\" senden wollen?", "MessageDownloadingEpisode": "Episode herunterladen", "MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 9195265ef..6befd10d8 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -218,6 +218,7 @@ "LabelCurrently": "Currently:", "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Datetime", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Description", "LabelDeselectAll": "Deselect All", "LabelDevice": "Device", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", "MessageDownloadingEpisode": "Downloading episode", "MessageDragFilesIntoTrackOrder": "Drag files into correct track order", diff --git a/client/strings/es.json b/client/strings/es.json index f03c63528..57d09a724 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -218,6 +218,7 @@ "LabelCurrently": "En este momento:", "LabelCustomCronExpression": "Expresión de Cron Personalizada:", "LabelDatetime": "Hora y Fecha", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Descripción", "LabelDeselectAll": "Deseleccionar Todos", "LabelDevice": "Dispositivo", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "¿Está seguro de que desea eliminar el respaldo {0}?", "MessageConfirmDeleteFile": "Esto eliminará el archivo de su sistema de archivos. ¿Está seguro?", "MessageConfirmDeleteLibrary": "¿Está seguro de que desea eliminar permanentemente la biblioteca \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?", "MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?", "MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?", "MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?", "MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?", "MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?", "MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?", "MessageConfirmRemoveCollection": "¿Está seguro de que desea remover la colección \"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "¿Está seguro de que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?", "MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe, por lo que se fusionarán.", "MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "¿Está seguro de que enviar {0} ebook(s) \"{1}\" al dispositivo \"{2}\"?", "MessageDownloadingEpisode": "Descargando Capitulo", "MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas.", diff --git a/client/strings/fr.json b/client/strings/fr.json index 031462b28..25b3261ef 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -218,6 +218,7 @@ "LabelCurrently": "En ce moment :", "LabelCustomCronExpression": "Expression cron personnalisée:", "LabelDatetime": "Datetime", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Description", "LabelDeselectAll": "Tout déselectionner", "LabelDevice": "Appareil", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la sauvegarde de « {0} » ?", "MessageConfirmDeleteFile": "Cela supprimera le fichier de votre système de fichiers. Êtes-vous sûr ?", "MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?", "MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une analyse forcée ?", "MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?", "MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr de vouloir marquer tous les épisodes comme non terminés ?", "MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme terminées ?", "MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme comme non terminés ?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » en « {1} » pour tous les articles ?", "MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.", "MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} « {1} » à l’appareil « {2} »?", "MessageDownloadingEpisode": "Téléchargement de l’épisode", "MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct", diff --git a/client/strings/gu.json b/client/strings/gu.json index 0803ccf47..7716773d8 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -218,6 +218,7 @@ "LabelCurrently": "Currently:", "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Datetime", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Description", "LabelDeselectAll": "Deselect All", "LabelDevice": "Device", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", "MessageDownloadingEpisode": "Downloading episode", "MessageDragFilesIntoTrackOrder": "Drag files into correct track order", diff --git a/client/strings/hi.json b/client/strings/hi.json index 1eea84954..3cc25ae60 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -218,6 +218,7 @@ "LabelCurrently": "Currently:", "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Datetime", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Description", "LabelDeselectAll": "Deselect All", "LabelDevice": "Device", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", "MessageDownloadingEpisode": "Downloading episode", "MessageDragFilesIntoTrackOrder": "Drag files into correct track order", diff --git a/client/strings/hr.json b/client/strings/hr.json index 47908b184..1a97f0f42 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -218,6 +218,7 @@ "LabelCurrently": "Trenutno:", "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Datetime", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Opis", "LabelDeselectAll": "Odznači sve", "LabelDevice": "Uređaj", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?", "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?", "MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", "MessageDownloadingEpisode": "Preuzimam epizodu", "MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.", diff --git a/client/strings/it.json b/client/strings/it.json index b60a87c1a..003e167c0 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -218,6 +218,7 @@ "LabelCurrently": "Attualmente:", "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Data & Ora", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Descrizione", "LabelDeselectAll": "Deseleziona Tutto", "LabelDevice": "Dispositivo", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?", "MessageConfirmDeleteFile": "Questo eliminerà il file dal tuo file system. Sei sicuro?", "MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?", "MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?", "MessageConfirmMarkAllEpisodesFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come finiti?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?", "MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Sei sicuro di voler rimuovere tutti i capitoli?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?", "MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.", "MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Sei sicuro di voler inviare {0} ebook \"{1}\" al Device \"{2}\"?", "MessageDownloadingEpisode": "Download episodio in corso", "MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto", diff --git a/client/strings/lt.json b/client/strings/lt.json index 31d259e64..3266e978d 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -218,6 +218,7 @@ "LabelCurrently": "Šiuo metu:", "LabelCustomCronExpression": "Nestandartinė Cron išraiška:", "LabelDatetime": "Data ir laikas", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Aprašymas", "LabelDeselectAll": "Išvalyti pasirinktus", "LabelDevice": "Įrenginys", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Ar tikrai norite ištrinti atsarginę kopiją, skirtą {0}?", "MessageConfirmDeleteFile": "Tai ištrins failą iš jūsų failų sistemos. Ar tikrai?", "MessageConfirmDeleteLibrary": "Ar tikrai norite visam laikui ištrinti biblioteką \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Ar tikrai norite ištrinti šią sesiją?", "MessageConfirmForceReScan": "Ar tikrai norite priversti perskenavimą?", "MessageConfirmMarkAllEpisodesFinished": "Ar tikrai norite pažymėti visus epizodus kaip užbaigtus?", "MessageConfirmMarkAllEpisodesNotFinished": "Ar tikrai norite pažymėti visus epizodus kaip nebaigtus?", "MessageConfirmMarkSeriesFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip užbaigtas?", "MessageConfirmMarkSeriesNotFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip nebaigtas?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Ar tikrai norite pašalinti visus skyrius?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveCollection": "Ar tikrai norite pašalinti kolekciją \"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Ar tikrai norite pervadinti žymą \"{0}\" į \"{1}\" visiems elementams?", "MessageConfirmRenameTagMergeNote": "Pastaba: ši žyma jau egzistuoja, todėl jos bus sujungtos.", "MessageConfirmRenameTagWarning": "Įspėjimas! Panaši žyma jau egzistuoja \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Ar tikrai norite nusiųsti {0} el. knygą \"{1}\" į įrenginį \"{2}\"?", "MessageDownloadingEpisode": "Epizodas atsisiunčiamas", "MessageDragFilesIntoTrackOrder": "Surikiuokite takelius vilkdami failus", diff --git a/client/strings/nl.json b/client/strings/nl.json index eb6b35b3a..da0b80460 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -218,6 +218,7 @@ "LabelCurrently": "Op dit moment:", "LabelCustomCronExpression": "Aangepaste Cron-uitdrukking:", "LabelDatetime": "Datum-tijd", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Beschrijving", "LabelDeselectAll": "Deselecteer alle", "LabelDevice": "Apparaat", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Weet je zeker dat je de backup voor {0} wil verwijderen?", "MessageConfirmDeleteFile": "Dit verwijdert het bestand uit het bestandssysteem. Weet je het zeker?", "MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?", "MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?", "MessageConfirmMarkAllEpisodesFinished": "Weet je zeker dat je alle afleveringen als voltooid wil markeren?", "MessageConfirmMarkAllEpisodesNotFinished": "Weet je zeker dat je alle afleveringen als niet-voltooid wil markeren?", "MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?", "MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?", "MessageConfirmRemoveAuthor": "Weet je zeker dat je auteur \"{0}\" wil verwijderen?", "MessageConfirmRemoveCollection": "Weet je zeker dat je de collectie \"{0}\" wil verwijderen?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Weet je zeker dat je tag \"{0}\" wil hernoemen naar\"{1}\" voor alle onderdelen?", "MessageConfirmRenameTagMergeNote": "Opmerking: Deze tag bestaat al, dus zullen ze worden samengevoegd.", "MessageConfirmRenameTagWarning": "Waarschuwing! Een gelijknamige tag met ander hoofdlettergebruik bestaat al: \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Weet je zeker dat je {0} ebook \"{1}\" naar apparaat \"{2}\" wil sturen?", "MessageDownloadingEpisode": "Aflevering aan het dowloaden", "MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde", diff --git a/client/strings/no.json b/client/strings/no.json index f4fe316c8..90e8758f3 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -218,6 +218,7 @@ "LabelCurrently": "Nåværende:", "LabelCustomCronExpression": "Tilpasset Cron utrykk:", "LabelDatetime": "Dato tid", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Beskrivelse", "LabelDeselectAll": "Fjern valg", "LabelDevice": "Enhet", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Er du sikker på at du vil slette sikkerhetskopi for {0}?", "MessageConfirmDeleteFile": "Dette vil slette filen fra filsystemet. Er du sikker?", "MessageConfirmDeleteLibrary": "Er du sikker på at du vil slette biblioteket \"{0}\" for godt?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Er du sikker på at du vil slette denne sesjonen?", "MessageConfirmForceReScan": "Er du sikker på at du vil tvinge en ny skann?", "MessageConfirmMarkAllEpisodesFinished": "Er du sikker på at du vil markere alle episodene som fullført?", "MessageConfirmMarkAllEpisodesNotFinished": "Er du sikker på at du vil markere alle episodene som ikke fullført?", "MessageConfirmMarkSeriesFinished": "Er du sikker på at du vil markere alle bøkene i serien som fullført?", "MessageConfirmMarkSeriesNotFinished": "Er du sikker på at du vil markere alle bøkene i serien som ikke fullført?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Er du sikker på at du vil fjerne alle kapitler?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveCollection": "Er du sikker på at du vil fjerne samling\"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Er du sikker på at du vil endre tag \"{0}\" til \"{1}\" for alle gjenstandene?", "MessageConfirmRenameTagMergeNote": "Notis: Denne taggen finnes allerede så de vil bli slått sammen.", "MessageConfirmRenameTagWarning": "Advarsel! En lignende tag eksisterer allerede (med forsjellige store / små bokstaver) \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Er du sikker på at du vil sende {0} ebok \"{1}\" til enhet \"{2}\"?", "MessageDownloadingEpisode": "Laster ned episode", "MessageDragFilesIntoTrackOrder": "Dra filene i rett spor rekkefølge", diff --git a/client/strings/pl.json b/client/strings/pl.json index a645877b8..82167c081 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -218,6 +218,7 @@ "LabelCurrently": "Obecnie:", "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Data i godzina", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Opis", "LabelDeselectAll": "Odznacz wszystko", "LabelDevice": "Urządzenie", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Czy na pewno chcesz usunąć kopię zapasową dla {0}?", "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?", "MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveCollection": "Czy na pewno chcesz usunąć kolekcję \"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", "MessageDownloadingEpisode": "Pobieranie odcinka", "MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów", diff --git a/client/strings/ru.json b/client/strings/ru.json index f7f569659..d4d258d31 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -218,6 +218,7 @@ "LabelCurrently": "Текущее:", "LabelCustomCronExpression": "Пользовательское выражение Cron:", "LabelDatetime": "Дата и время", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "Описание", "LabelDeselectAll": "Снять выделение", "LabelDevice": "Устройство", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "Вы уверены, что хотите удалить бэкап для {0}?", "MessageConfirmDeleteFile": "Это удалит файл из Вашей файловой системы. Вы уверены?", "MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?", "MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?", "MessageConfirmMarkAllEpisodesFinished": "Вы уверены, что хотите отметить все эпизоды как завершенные?", "MessageConfirmMarkAllEpisodesNotFinished": "Вы уверены, что хотите отметить все эпизоды как не завершенные?", "MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как завершенные?", "MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как не завершенные?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "Вы уверены, что хотите переименовать тег \"{0}\" в \"{1}\" для всех элементов?", "MessageConfirmRenameTagMergeNote": "Примечание: Этот тег уже существует, поэтому они будут объединены.", "MessageConfirmRenameTagWarning": "Предупреждение! Похожий тег с другими начальными буквами уже существует \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "Вы уверены, что хотите отправить {0} e-книгу \"{1}\" на устройство \"{2}\"?", "MessageDownloadingEpisode": "Эпизод скачивается", "MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index f5a32ff4f..b76e79494 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -218,6 +218,7 @@ "LabelCurrently": "当前:", "LabelCustomCronExpression": "自定义计划任务表达式:", "LabelDatetime": "日期时间", + "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDescription": "描述", "LabelDeselectAll": "全部取消选择", "LabelDevice": "设备", @@ -522,12 +523,15 @@ "MessageConfirmDeleteBackup": "你确定要删除备份 {0}?", "MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?", "MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteSession": "你确定要删除此会话吗?", "MessageConfirmForceReScan": "你确定要强制重新扫描吗?", "MessageConfirmMarkAllEpisodesFinished": "你确定要将所有剧集都标记为已完成吗?", "MessageConfirmMarkAllEpisodesNotFinished": "你确定要将所有剧集都标记为未完成吗?", "MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?", "MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?", + "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?", "MessageConfirmRemoveAuthor": "你确定要删除作者 \"{0}\"?", "MessageConfirmRemoveCollection": "你确定要移除收藏 \"{0}\"?", @@ -541,6 +545,7 @@ "MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?", "MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.", "MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?", "MessageDownloadingEpisode": "正在下载剧集", "MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序", From 6f653502694717644f9d6fe4c6bdb3d582186b17 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 20 Oct 2023 16:39:32 -0500 Subject: [PATCH 071/208] Update:JSDocs for task manager --- server/Server.js | 8 +++---- server/controllers/MiscController.js | 4 +++- server/managers/AbMergeManager.js | 9 ++++--- server/managers/AudioMetadataManager.js | 10 ++++---- server/managers/PodcastManager.js | 9 +++---- server/managers/TaskManager.js | 14 ++++++++++- server/objects/Task.js | 32 ++++++++++++++++++++++++- server/routers/ApiRouter.js | 1 - 8 files changed, 64 insertions(+), 23 deletions(-) diff --git a/server/Server.js b/server/Server.js index 36780df4a..f5bb85a01 100644 --- a/server/Server.js +++ b/server/Server.js @@ -31,7 +31,6 @@ const PodcastManager = require('./managers/PodcastManager') const AudioMetadataMangaer = require('./managers/AudioMetadataManager') const RssFeedManager = require('./managers/RssFeedManager') const CronManager = require('./managers/CronManager') -const TaskManager = require('./managers/TaskManager') const LibraryScanner = require('./scanner/LibraryScanner') class Server { @@ -58,15 +57,14 @@ class Server { this.auth = new Auth() // Managers - this.taskManager = new TaskManager() this.notificationManager = new NotificationManager() this.emailManager = new EmailManager() this.backupManager = new BackupManager() this.logManager = new LogManager() - this.abMergeManager = new AbMergeManager(this.taskManager) + this.abMergeManager = new AbMergeManager() this.playbackSessionManager = new PlaybackSessionManager() - this.podcastManager = new PodcastManager(this.watcher, this.notificationManager, this.taskManager) - this.audioMetadataManager = new AudioMetadataMangaer(this.taskManager) + this.podcastManager = new PodcastManager(this.watcher, this.notificationManager) + this.audioMetadataManager = new AudioMetadataMangaer() this.rssFeedManager = new RssFeedManager() this.cronManager = new CronManager(this.podcastManager) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 0fa1c62fe..ffa4e2c2a 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -9,6 +9,8 @@ const libraryItemFilters = require('../utils/queries/libraryItemFilters') const patternValidation = require('../libs/nodeCron/pattern-validation') const { isObject, getTitleIgnorePrefix } = require('../utils/index') +const TaskManager = require('../managers/TaskManager') + // // This is a controller for routes that don't have a home yet :( // @@ -102,7 +104,7 @@ class MiscController { const includeArray = (req.query.include || '').split(',') const data = { - tasks: this.taskManager.tasks.map(t => t.toJSON()) + tasks: TaskManager.tasks.map(t => t.toJSON()) } if (includeArray.includes('queue')) { diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js index 4ced13900..8a87df2ec 100644 --- a/server/managers/AbMergeManager.js +++ b/server/managers/AbMergeManager.js @@ -4,14 +4,13 @@ const fs = require('../libs/fsExtra') const workerThreads = require('worker_threads') const Logger = require('../Logger') +const TaskManager = require('./TaskManager') const Task = require('../objects/Task') const { writeConcatFile } = require('../utils/ffmpegHelpers') const toneHelpers = require('../utils/toneHelpers') class AbMergeManager { - constructor(taskManager) { - this.taskManager = taskManager - + constructor() { this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items') this.pendingTasks = [] @@ -45,7 +44,7 @@ class AbMergeManager { } const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.` task.setData('encode-m4b', 'Encoding M4b', taskDescription, false, taskData) - this.taskManager.addTask(task) + TaskManager.addTask(task) Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`) if (!await fs.pathExists(taskData.itemCachePath)) { @@ -234,7 +233,7 @@ class AbMergeManager { } } - this.taskManager.taskFinished(task) + TaskManager.taskFinished(task) } } module.exports = AbMergeManager diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 2f74bcbe9..11c828226 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -7,12 +7,12 @@ const fs = require('../libs/fsExtra') const toneHelpers = require('../utils/toneHelpers') +const TaskManager = require('./TaskManager') + const Task = require('../objects/Task') class AudioMetadataMangaer { - constructor(taskManager) { - this.taskManager = taskManager - + constructor() { this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items') this.MAX_CONCURRENT_TASKS = 1 @@ -101,7 +101,7 @@ class AudioMetadataMangaer { async runMetadataEmbed(task) { this.tasksRunning.push(task) - this.taskManager.addTask(task) + TaskManager.addTask(task) Logger.info(`[AudioMetadataManager] Starting metadata embed task`, task.description) @@ -176,7 +176,7 @@ class AudioMetadataMangaer { } handleTaskFinished(task) { - this.taskManager.taskFinished(task) + TaskManager.taskFinished(task) this.tasksRunning = this.tasksRunning.filter(t => t.id !== task.id) if (this.tasksRunning.length < this.MAX_CONCURRENT_TASKS && this.tasksQueued.length) { diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index b88a38afd..7b6ff845b 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -12,6 +12,8 @@ const opmlGenerator = require('../utils/generators/opmlGenerator') const prober = require('../utils/prober') const ffmpegHelpers = require('../utils/ffmpegHelpers') +const TaskManager = require('./TaskManager') + const LibraryFile = require('../objects/files/LibraryFile') const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload') const PodcastEpisode = require('../objects/entities/PodcastEpisode') @@ -19,10 +21,9 @@ const AudioFile = require('../objects/files/AudioFile') const Task = require("../objects/Task") class PodcastManager { - constructor(watcher, notificationManager, taskManager) { + constructor(watcher, notificationManager) { this.watcher = watcher this.notificationManager = notificationManager - this.taskManager = taskManager this.downloadQueue = [] this.currentDownload = null @@ -76,7 +77,7 @@ class PodcastManager { libraryItemId: podcastEpisodeDownload.libraryItemId, } task.setData('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData) - this.taskManager.addTask(task) + TaskManager.addTask(task) SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient()) this.currentDownload = podcastEpisodeDownload @@ -128,7 +129,7 @@ class PodcastManager { this.currentDownload.setFinished(false) } - this.taskManager.taskFinished(task) + TaskManager.taskFinished(task) SocketAuthority.emitter('episode_download_finished', this.currentDownload.toJSONForClient()) SocketAuthority.emitter('episode_download_queue_updated', this.getDownloadQueueDetails()) diff --git a/server/managers/TaskManager.js b/server/managers/TaskManager.js index 38e8b5804..747ded08f 100644 --- a/server/managers/TaskManager.js +++ b/server/managers/TaskManager.js @@ -1,15 +1,27 @@ const SocketAuthority = require('../SocketAuthority') +const Task = require('../objects/Task') class TaskManager { constructor() { + /** @type {Task[]} */ this.tasks = [] } + /** + * Add task and emit socket task_started event + * + * @param {Task} task + */ addTask(task) { this.tasks.push(task) SocketAuthority.emitter('task_started', task.toJSON()) } + /** + * Remove task and emit task_finished event + * + * @param {Task} task + */ taskFinished(task) { if (this.tasks.some(t => t.id === task.id)) { this.tasks = this.tasks.filter(t => t.id !== task.id) @@ -17,4 +29,4 @@ class TaskManager { } } } -module.exports = TaskManager \ No newline at end of file +module.exports = new TaskManager() \ No newline at end of file diff --git a/server/objects/Task.js b/server/objects/Task.js index 04c83d174..db7e490e6 100644 --- a/server/objects/Task.js +++ b/server/objects/Task.js @@ -2,19 +2,30 @@ const uuidv4 = require("uuid").v4 class Task { constructor() { + /** @type {string} */ this.id = null + /** @type {string} */ this.action = null // e.g. embed-metadata, encode-m4b, etc + /** @type {Object} custom data */ this.data = null // additional info for the action like libraryItemId + /** @type {string} */ this.title = null + /** @type {string} */ this.description = null + /** @type {string} */ this.error = null - this.showSuccess = false // If true client side should keep the task visible after success + /** @type {boolean} client should keep the task visible after success */ + this.showSuccess = false + /** @type {boolean} */ this.isFailed = false + /** @type {boolean} */ this.isFinished = false + /** @type {number} */ this.startedAt = null + /** @type {number} */ this.finishedAt = null } @@ -34,6 +45,15 @@ class Task { } } + /** + * Set initial task data + * + * @param {string} action + * @param {string} title + * @param {string} description + * @param {boolean} showSuccess + * @param {Object} [data] + */ setData(action, title, description, showSuccess, data = {}) { this.id = uuidv4() this.action = action @@ -44,6 +64,11 @@ class Task { this.startedAt = Date.now() } + /** + * Set task as failed + * + * @param {string} message error message + */ setFailed(message) { this.error = message this.isFailed = true @@ -51,6 +76,11 @@ class Task { this.setFinished() } + /** + * Set task as finished + * + * @param {string} [newDescription] update description + */ setFinished(newDescription = null) { if (newDescription) { this.description = newDescription diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 034951df5..b40c3d801 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -46,7 +46,6 @@ class ApiRouter { this.cronManager = Server.cronManager this.notificationManager = Server.notificationManager this.emailManager = Server.emailManager - this.taskManager = Server.taskManager this.router = express() this.router.disable('x-powered-by') From bef6549805eba03afd6d68887f4614939a876474 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 20 Oct 2023 17:46:18 -0500 Subject: [PATCH 072/208] Update:Replace library scan toast with task manager #1279 --- .../components/cards/ItemTaskRunningCard.vue | 23 +++++++++++++++---- client/layouts/default.vue | 7 ------ server/managers/PodcastManager.js | 5 +--- server/managers/TaskManager.js | 16 +++++++++++++ server/scanner/LibraryScan.js | 11 ++++++++- server/scanner/LibraryScanner.js | 15 +++++++++--- server/utils/index.js | 3 +++ 7 files changed, 61 insertions(+), 19 deletions(-) diff --git a/client/components/cards/ItemTaskRunningCard.vue b/client/components/cards/ItemTaskRunningCard.vue index 63a3644da..c9de1a87f 100644 --- a/client/components/cards/ItemTaskRunningCard.vue +++ b/client/components/cards/ItemTaskRunningCard.vue @@ -1,10 +1,8 @@ <template> <div class="flex items-center px-1 overflow-hidden"> <div class="w-8 flex items-center justify-center"> - <!-- <div class="text-lg"> --> <span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{ actionIcon }}</span> <widgets-loading-spinner v-else /> - <!-- </div> --> </div> <div class="flex-grow px-2 taskRunningCardContent"> <p class="truncate text-sm">{{ title }}</p> @@ -12,7 +10,9 @@ <p class="truncate text-xs text-gray-300">{{ description }}</p> <p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p> + <p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p> </div> + <ui-btn v-if="userIsAdminOrUp && !isFinished && action === 'library-scan' && !cancelingScan" color="primary" :padding-y="1" :padding-x="1" class="text-xs w-16 max-w-16 truncate mr-1" @click.stop="cancelScan">{{ this.$strings.ButtonCancel }}</ui-btn> </div> </template> @@ -25,9 +25,14 @@ export default { } }, data() { - return {} + return { + cancelingScan: false + } }, computed: { + userIsAdminOrUp() { + return this.$store.getters['user/getIsAdminOrUp'] + }, title() { return this.task.title || 'No Title' }, @@ -78,7 +83,17 @@ export default { return '' } }, - methods: {}, + methods: { + cancelScan() { + const libraryId = this.task?.data?.libraryId + if (!libraryId) { + console.error('No library id in library-scan task', this.task) + return + } + this.cancelingScan = true + this.$root.socket.emit('cancel_scan', libraryId) + } + }, mounted() {} } </script> diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 2817f23f8..5ff344396 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -123,13 +123,6 @@ export default { init(payload) { console.log('Init Payload', payload) - // Start scans currently running - if (payload.librariesScanning) { - payload.librariesScanning.forEach((libraryScan) => { - this.scanStart(libraryScan) - }) - } - // Remove any current scans that are no longer running var currentScans = [...this.$store.state.scanners.libraryScans] currentScans.forEach((ls) => { diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index 7b6ff845b..6e91a0aa9 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -18,7 +18,6 @@ const LibraryFile = require('../objects/files/LibraryFile') const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload') const PodcastEpisode = require('../objects/entities/PodcastEpisode') const AudioFile = require('../objects/files/AudioFile') -const Task = require("../objects/Task") class PodcastManager { constructor(watcher, notificationManager) { @@ -70,14 +69,12 @@ class PodcastManager { return } - const task = new Task() const taskDescription = `Downloading episode "${podcastEpisodeDownload.podcastEpisode.title}".` const taskData = { libraryId: podcastEpisodeDownload.libraryId, libraryItemId: podcastEpisodeDownload.libraryItemId, } - task.setData('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData) - TaskManager.addTask(task) + const task = TaskManager.createAndAddTask('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData) SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient()) this.currentDownload = podcastEpisodeDownload diff --git a/server/managers/TaskManager.js b/server/managers/TaskManager.js index 747ded08f..31cf06a13 100644 --- a/server/managers/TaskManager.js +++ b/server/managers/TaskManager.js @@ -28,5 +28,21 @@ class TaskManager { SocketAuthority.emitter('task_finished', task.toJSON()) } } + + /** + * Create new task and add + * + * @param {string} action + * @param {string} title + * @param {string} description + * @param {boolean} showSuccess + * @param {Object} [data] + */ + createAndAddTask(action, title, description, showSuccess, data = {}) { + const task = new Task() + task.setData(action, title, description, showSuccess, data) + this.addTask(task) + return task + } } module.exports = new TaskManager() \ No newline at end of file diff --git a/server/scanner/LibraryScan.js b/server/scanner/LibraryScan.js index 88562e2c4..1dc945fb2 100644 --- a/server/scanner/LibraryScan.js +++ b/server/scanner/LibraryScan.js @@ -6,7 +6,7 @@ const date = require('../libs/dateAndTime') const Logger = require('../Logger') const Library = require('../objects/Library') const { LogLevel } = require('../utils/constants') -const { secondsToTimestamp } = require('../utils/index') +const { secondsToTimestamp, elapsedPretty } = require('../utils/index') class LibraryScan { constructor() { @@ -67,6 +67,15 @@ class LibraryScan { get logFilename() { return date.format(new Date(), 'YYYY-MM-DD') + '_' + this.id + '.txt' } + get scanResultsString() { + if (this.error) return this.error + const strs = [] + if (this.resultsAdded) strs.push(`${this.resultsAdded} added`) + if (this.resultsUpdated) strs.push(`${this.resultsUpdated} updated`) + if (this.resultsMissing) strs.push(`${this.resultsMissing} missing`) + if (!strs.length) return `Everything was up to date (${elapsedPretty(this.elapsed / 1000)})` + return strs.join(', ') + ` (${elapsedPretty(this.elapsed / 1000)})` + } toJSON() { return { diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index 44ccdd05e..b7f9093ef 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -9,6 +9,7 @@ const fileUtils = require('../utils/fileUtils') const scanUtils = require('../utils/scandir') const { LogLevel, ScanResult } = require('../utils/constants') const libraryFilters = require('../utils/queries/libraryFilters') +const TaskManager = require('../managers/TaskManager') const LibraryItemScanner = require('./LibraryItemScanner') const LibraryScan = require('./LibraryScan') const LibraryItemScanData = require('./LibraryItemScanData') @@ -68,7 +69,12 @@ class LibraryScanner { libraryScan.verbose = true this.librariesScanning.push(libraryScan.getScanEmitData) - SocketAuthority.emitter('scan_start', libraryScan.getScanEmitData) + const taskData = { + libraryId: library.id, + libraryName: library.name, + libraryMediaType: library.mediaType + } + const task = TaskManager.createAndAddTask('library-scan', `Scanning "${library.name}" library`, null, true, taskData) Logger.info(`[LibraryScanner] Starting${forceRescan ? ' (forced)' : ''} library scan ${libraryScan.id} for ${libraryScan.libraryName}`) @@ -85,9 +91,11 @@ class LibraryScanner { this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id) if (canceled && !libraryScan.totalResults) { + task.setFinished('Scan canceled') + TaskManager.taskFinished(task) + const emitData = libraryScan.getScanEmitData emitData.results = null - SocketAuthority.emitter('scan_complete', emitData) return } @@ -98,7 +106,8 @@ class LibraryScanner { } await Database.libraryModel.updateFromOld(library) - SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData) + task.setFinished(libraryScan.scanResultsString) + TaskManager.taskFinished(task) if (libraryScan.totalResults) { libraryScan.saveLog() diff --git a/server/utils/index.js b/server/utils/index.js index abcc626c0..84167229a 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -65,6 +65,9 @@ module.exports.getId = (prepend = '') => { } function elapsedPretty(seconds) { + if (seconds > 0 && seconds < 1) { + return `${Math.floor(seconds * 1000)} ms` + } if (seconds < 60) { return `${Math.floor(seconds)} sec` } From d7264f8c2247d32d8f23cfce2b0d72d3dfdf2725 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 21 Oct 2023 12:25:45 -0500 Subject: [PATCH 073/208] Update watcher scanner to show task notification --- .../components/tables/library/LibraryItem.vue | 2 +- server/Watcher.js | 16 ++++++- server/scanner/LibraryScanner.js | 43 ++++++++++++++++--- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/client/components/tables/library/LibraryItem.vue b/client/components/tables/library/LibraryItem.vue index 25b581c99..8dd3e2609 100644 --- a/client/components/tables/library/LibraryItem.vue +++ b/client/components/tables/library/LibraryItem.vue @@ -125,7 +125,7 @@ export default { this.$store .dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force }) .then(() => { - this.$toast.success(this.$strings.ToastLibraryScanStarted) + // this.$toast.success(this.$strings.ToastLibraryScanStarted) }) .catch((error) => { console.error('Failed to start scan', error) diff --git a/server/Watcher.js b/server/Watcher.js index 577460a4c..3ce6a5f5d 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -3,6 +3,8 @@ const EventEmitter = require('events') const Watcher = require('./libs/watcher/watcher') const Logger = require('./Logger') const LibraryScanner = require('./scanner/LibraryScanner') +const Task = require('./objects/Task') +const TaskManager = require('./managers/TaskManager') const { filePathToPOSIX } = require('./utils/fileUtils') @@ -22,7 +24,10 @@ class FolderWatcher extends EventEmitter { /** @type {PendingFileUpdate[]} */ this.pendingFileUpdates = [] this.pendingDelay = 4000 + /** @type {NodeJS.Timeout} */ this.pendingTimeout = null + /** @type {Task} */ + this.pendingTask = null /** @type {string[]} */ this.ignoreDirs = [] @@ -202,6 +207,13 @@ class FolderWatcher extends EventEmitter { Logger.debug(`[Watcher] Modified file in library "${libwatcher.name}" and folder "${folder.id}" with relPath "${relPath}"`) + if (!this.pendingTask) { + const taskData = { + libraryId, + libraryName: libwatcher.name + } + this.pendingTask = TaskManager.createAndAddTask('watcher-scan', `Scanning file changes in "${libwatcher.name}"`, null, true, taskData) + } this.pendingFileUpdates.push({ path, relPath, @@ -213,8 +225,8 @@ class FolderWatcher extends EventEmitter { // Notify server of update after "pendingDelay" clearTimeout(this.pendingTimeout) this.pendingTimeout = setTimeout(() => { - // this.emit('files', this.pendingFileUpdates) - LibraryScanner.scanFilesChanged(this.pendingFileUpdates) + LibraryScanner.scanFilesChanged(this.pendingFileUpdates, this.pendingTask) + this.pendingTask = null this.pendingFileUpdates = [] }, this.pendingDelay) } diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index b7f9093ef..11a88bd4b 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -13,6 +13,7 @@ const TaskManager = require('../managers/TaskManager') const LibraryItemScanner = require('./LibraryItemScanner') const LibraryScan = require('./LibraryScan') const LibraryItemScanData = require('./LibraryItemScanData') +const Task = require('../objects/Task') class LibraryScanner { constructor() { @@ -20,7 +21,7 @@ class LibraryScanner { this.librariesScanning = [] this.scanningFilesChanged = false - /** @type {import('../Watcher').PendingFileUpdate[][]} */ + /** @type {[import('../Watcher').PendingFileUpdate[], Task][]} */ this.pendingFileUpdatesToScan = [] } @@ -335,18 +336,25 @@ class LibraryScanner { /** * Scan files changed from Watcher * @param {import('../Watcher').PendingFileUpdate[]} fileUpdates + * @param {Task} pendingTask */ - async scanFilesChanged(fileUpdates) { + async scanFilesChanged(fileUpdates, pendingTask) { if (!fileUpdates?.length) return // If already scanning files from watcher then add these updates to queue if (this.scanningFilesChanged) { - this.pendingFileUpdatesToScan.push(fileUpdates) + this.pendingFileUpdatesToScan.push([fileUpdates, pendingTask]) Logger.debug(`[LibraryScanner] Already scanning files from watcher - file updates pushed to queue (size ${this.pendingFileUpdatesToScan.length})`) return } this.scanningFilesChanged = true + const results = { + added: 0, + updated: 0, + removed: 0 + } + // files grouped by folder const folderGroups = this.getFileUpdatesGrouped(fileUpdates) @@ -377,17 +385,42 @@ class LibraryScanner { const folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup) Logger.debug(`[LibraryScanner] Folder scan results`, folderScanResults) + // Tally results to share with client + let resetFilterData = false + Object.values(folderScanResults).forEach((scanResult) => { + if (scanResult === ScanResult.ADDED) { + resetFilterData = true + results.added++ + } else if (scanResult === ScanResult.REMOVED) { + resetFilterData = true + results.removed++ + } else if (scanResult === ScanResult.UPDATED) { + resetFilterData = true + results.updated++ + } + }) + // If something was updated then reset numIssues filter data for library - if (Object.values(folderScanResults).some(scanResult => scanResult !== ScanResult.NOTHING && scanResult !== ScanResult.UPTODATE)) { + if (resetFilterData) { await Database.resetLibraryIssuesFilterData(libraryId) } } + // Complete task and send results to client + const resultStrs = [] + if (results.added) resultStrs.push(`${results.added} added`) + if (results.updated) resultStrs.push(`${results.updated} updated`) + if (results.removed) resultStrs.push(`${results.removed} missing`) + let scanResultStr = 'Scan finished with no changes' + if (resultStrs.length) scanResultStr = resultStrs.join(', ') + pendingTask.setFinished(scanResultStr) + TaskManager.taskFinished(pendingTask) + this.scanningFilesChanged = false if (this.pendingFileUpdatesToScan.length) { Logger.debug(`[LibraryScanner] File updates finished scanning with more updates in queue (${this.pendingFileUpdatesToScan.length})`) - this.scanFilesChanged(this.pendingFileUpdatesToScan.shift()) + this.scanFilesChanged(...this.pendingFileUpdatesToScan.shift()) } } From 58b9a42c843a9c95de85af40eb8a493f1af0c2a4 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 21 Oct 2023 12:56:35 -0500 Subject: [PATCH 074/208] Add:Scan button on libraries table --- client/components/tables/library/LibraryItem.vue | 16 +++++++++++----- client/layouts/default.vue | 9 --------- client/store/scanners.js | 2 +- client/store/tasks.js | 5 ++++- server/Server.js | 4 ---- server/SocketAuthority.js | 3 +-- 6 files changed, 17 insertions(+), 22 deletions(-) diff --git a/client/components/tables/library/LibraryItem.vue b/client/components/tables/library/LibraryItem.vue index 8dd3e2609..6a0cb36f8 100644 --- a/client/components/tables/library/LibraryItem.vue +++ b/client/components/tables/library/LibraryItem.vue @@ -1,7 +1,7 @@ <template> <div class="w-full pl-2 pr-4 md:px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false"> <div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" /> - <ui-library-icon v-if="!libraryScan" :icon="library.icon" :size="6" font-size="lg md:text-xl" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" /> + <ui-library-icon v-if="!isScanning" :icon="library.icon" :size="6" font-size="lg md:text-xl" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" /> <svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin"> <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" /> </svg> @@ -9,11 +9,14 @@ <div class="flex-grow" /> + <!-- Scan button only shown on desktop --> + <ui-btn v-if="!isScanning && !isDeleting" color="bg" class="hidden md:block mx-2 text-xs" :padding-y="1" :padding-x="3" @click.stop="scanBtnClick">{{ this.$strings.ButtonScan }}</ui-btn> + <!-- Desktop context menu icon --> - <ui-context-menu-dropdown v-if="!libraryScan && !isDeleting" :items="contextMenuItems" :icon-class="`text-1.5xl text-gray-${isHovering ? 50 : 400}`" class="!hidden md:!block" @action="contextMenuAction" /> + <ui-context-menu-dropdown v-if="!isScanning && !isDeleting" :items="contextMenuItems" :icon-class="`text-1.5xl text-gray-${isHovering ? 50 : 400}`" class="!hidden md:!block" @action="contextMenuAction" /> <!-- Mobile context menu icon --> - <span v-if="!libraryScan && !isDeleting" class="!block md:!hidden material-icons text-xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span> + <span v-if="!isScanning && !isDeleting" class="!block md:!hidden material-icons text-xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span> <div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin"> <svg viewBox="0 0 24 24" class="w-6 h-6"> @@ -48,8 +51,8 @@ export default { isHovering() { return this.mouseover && !this.dragging }, - libraryScan() { - return this.$store.getters['scanners/getLibraryScan'](this.library.id) + isScanning() { + return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.library.id) }, mediaType() { return this.library.mediaType @@ -89,6 +92,9 @@ export default { } }, methods: { + scanBtnClick() { + this.scan() + }, contextMenuAction({ action }) { this.showMobileMenu = false if (action === 'edit') { diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 5ff344396..1f2acbd32 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -123,15 +123,6 @@ export default { init(payload) { console.log('Init Payload', payload) - // Remove any current scans that are no longer running - var currentScans = [...this.$store.state.scanners.libraryScans] - currentScans.forEach((ls) => { - if (!payload.librariesScanning || !payload.librariesScanning.find((_ls) => _ls.id === ls.id)) { - this.$toast.dismiss(ls.toastId) - this.$store.commit('scanners/remove', ls) - } - }) - if (payload.usersOnline) { this.$store.commit('users/setUsersOnline', payload.usersOnline) } diff --git a/client/store/scanners.js b/client/store/scanners.js index 9a330f4ff..de154c3d5 100644 --- a/client/store/scanners.js +++ b/client/store/scanners.js @@ -84,7 +84,7 @@ export const actions = { export const mutations = { addUpdate(state, data) { - var index = state.libraryScans.findIndex(lib => lib.id === data.id) + const index = state.libraryScans.findIndex(lib => lib.id === data.id) if (index >= 0) { state.libraryScans.splice(index, 1, data) } else { diff --git a/client/store/tasks.js b/client/store/tasks.js index e8422c77a..9277d412e 100644 --- a/client/store/tasks.js +++ b/client/store/tasks.js @@ -6,7 +6,10 @@ export const state = () => ({ export const getters = { getTasksByLibraryItemId: (state) => (libraryItemId) => { - return state.tasks.filter(t => t.data && t.data.libraryItemId === libraryItemId) + return state.tasks.filter(t => t.data?.libraryItemId === libraryItemId) + }, + getRunningLibraryScanTask: (state) => (libraryId) => { + return state.tasks.find(t => t.data?.libraryId === libraryId && !t.isFinished) } } diff --git a/server/Server.js b/server/Server.js index f5bb85a01..d95bd7997 100644 --- a/server/Server.js +++ b/server/Server.js @@ -86,10 +86,6 @@ class Server { LibraryScanner.setCancelLibraryScan(libraryId) } - getLibrariesScanning() { - return LibraryScanner.librariesScanning - } - /** * Initialize database, backups, logs, rss feeds, cron jobs & watcher * Cleanup stale/invalid data diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 86f94d3d8..ea84e7df9 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -179,8 +179,7 @@ class SocketAuthority { const initialPayload = { userId: client.user.id, - username: client.user.username, - librariesScanning: this.Server.getLibrariesScanning() + username: client.user.username } if (user.isAdminOrUp) { initialPayload.usersOnline = this.getUsersOnline() From 50215dab9a905bbdc2d8dc1ef905c604e936ea66 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 21 Oct 2023 13:00:41 -0500 Subject: [PATCH 075/208] Hide library modal tools tab for new libraries --- client/components/modals/libraries/EditModal.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/components/modals/libraries/EditModal.vue b/client/components/modals/libraries/EditModal.vue index 03b66931b..5bcdabede 100644 --- a/client/components/modals/libraries/EditModal.vue +++ b/client/components/modals/libraries/EditModal.vue @@ -88,6 +88,8 @@ export default { component: 'modals-libraries-library-tools' } ].filter((tab) => { + // Do not show tools tab for new libraries + if (tab.id === 'tools' && !this.library) return false return tab.id !== 'scanner' || this.mediaType === 'book' }) }, From 49403771c95e8e1d7e0dce868b0db99197a46ace Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 21 Oct 2023 13:53:00 -0500 Subject: [PATCH 076/208] Update:Quick match all for library to use task instead of toast, remove scan socket events --- .../components/cards/ItemTaskRunningCard.vue | 5 +- .../components/modals/item/tabs/Details.vue | 8 +-- .../tables/library/LibrariesTable.vue | 5 +- client/layouts/default.vue | 53 ------------------- client/store/scanners.js | 25 ++------- client/store/tasks.js | 3 +- server/controllers/LibraryController.js | 7 +++ server/models/LibraryItem.js | 2 +- server/scanner/Scanner.js | 26 +++++++-- 9 files changed, 45 insertions(+), 89 deletions(-) diff --git a/client/components/cards/ItemTaskRunningCard.vue b/client/components/cards/ItemTaskRunningCard.vue index c9de1a87f..d284c5059 100644 --- a/client/components/cards/ItemTaskRunningCard.vue +++ b/client/components/cards/ItemTaskRunningCard.vue @@ -12,7 +12,7 @@ <p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p> <p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p> </div> - <ui-btn v-if="userIsAdminOrUp && !isFinished && action === 'library-scan' && !cancelingScan" color="primary" :padding-y="1" :padding-x="1" class="text-xs w-16 max-w-16 truncate mr-1" @click.stop="cancelScan">{{ this.$strings.ButtonCancel }}</ui-btn> + <ui-btn v-if="userIsAdminOrUp && !isFinished && isLibraryScan && !cancelingScan" color="primary" :padding-y="1" :padding-x="1" class="text-xs w-16 max-w-16 truncate mr-1" @click.stop="cancelScan">{{ this.$strings.ButtonCancel }}</ui-btn> </div> </template> @@ -81,6 +81,9 @@ export default { } return '' + }, + isLibraryScan() { + return this.action === 'library-scan' || this.action === 'library-match-all' } }, methods: { diff --git a/client/components/modals/item/tabs/Details.vue b/client/components/modals/item/tabs/Details.vue index 14fe68a78..62f08c92c 100644 --- a/client/components/modals/item/tabs/Details.vue +++ b/client/components/modals/item/tabs/Details.vue @@ -11,8 +11,8 @@ <ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">{{ $strings.ButtonQuickMatch }}</ui-btn> </ui-tooltip> - <ui-tooltip :disabled="!!libraryScan" text="Rescan library item including metadata" direction="bottom" class="mr-2 md:mr-4"> - <ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">{{ $strings.ButtonReScan }}</ui-btn> + <ui-tooltip :disabled="isLibraryScanning" text="Rescan library item including metadata" direction="bottom" class="mr-2 md:mr-4"> + <ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="isLibraryScanning" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">{{ $strings.ButtonReScan }}</ui-btn> </ui-tooltip> <div class="flex-grow" /> @@ -80,9 +80,9 @@ export default { libraryProvider() { return this.$store.getters['libraries/getLibraryProvider'](this.libraryId) || 'google' }, - libraryScan() { + isLibraryScanning() { if (!this.libraryId) return null - return this.$store.getters['scanners/getLibraryScan'](this.libraryId) + return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.libraryId) } }, methods: { diff --git a/client/components/tables/library/LibrariesTable.vue b/client/components/tables/library/LibrariesTable.vue index 598b12b7c..faf8d69d2 100644 --- a/client/components/tables/library/LibrariesTable.vue +++ b/client/components/tables/library/LibrariesTable.vue @@ -42,13 +42,10 @@ export default { return this.$store.getters['libraries/getCurrentLibrary'] }, currentLibraryId() { - return this.currentLibrary ? this.currentLibrary.id : null + return this.currentLibrary?.id || null }, libraries() { return this.$store.getters['libraries/getSortedLibraries']() - }, - libraryScans() { - return this.$store.state.scanners.libraryScans } }, methods: { diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 1f2acbd32..4f5e0feaa 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -212,54 +212,6 @@ export default { this.libraryItemAdded(ab) }) }, - scanComplete(data) { - console.log('Scan complete received', data) - - let message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" complete!` - let toastType = 'success' - if (data.error) { - message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" finished with error:\n${data.error}` - toastType = 'error' - } else if (data.results) { - var scanResultMsgs = [] - var results = data.results - if (results.added) scanResultMsgs.push(`${results.added} added`) - if (results.updated) scanResultMsgs.push(`${results.updated} updated`) - if (results.removed) scanResultMsgs.push(`${results.removed} removed`) - if (results.missing) scanResultMsgs.push(`${results.missing} missing`) - if (!scanResultMsgs.length) message += '\nEverything was up to date' - else message += '\n' + scanResultMsgs.join('\n') - } else { - message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" was canceled` - } - - var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id) - if (existingScan && !isNaN(existingScan.toastId)) { - this.$toast.update(existingScan.toastId, { content: message, options: { timeout: 5000, type: toastType, closeButton: false, onClose: () => null } }, true) - } else { - this.$toast[toastType](message, { timeout: 5000 }) - } - - this.$store.commit('scanners/remove', data) - }, - onScanToastCancel(id) { - this.$root.socket.emit('cancel_scan', id) - }, - scanStart(data) { - data.toastId = this.$toast(`${data.type === 'match' ? 'Matching' : 'Scanning'} "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, onClose: () => this.onScanToastCancel(data.id) }) - this.$store.commit('scanners/addUpdate', data) - }, - scanProgress(data) { - var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id) - if (existingScan && !isNaN(existingScan.toastId)) { - data.toastId = existingScan.toastId - this.$toast.update(existingScan.toastId, { content: `Scanning "${existingScan.name}"... ${data.progress.progress || 0}%`, options: { timeout: false } }, true) - } else { - data.toastId = this.$toast(`Scanning "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, onClose: () => this.onScanToastCancel(data.id) }) - } - - this.$store.commit('scanners/addUpdate', data) - }, taskStarted(task) { console.log('Task started', task) this.$store.commit('tasks/addUpdateTask', task) @@ -442,11 +394,6 @@ export default { this.socket.on('playlist_updated', this.playlistUpdated) this.socket.on('playlist_removed', this.playlistRemoved) - // Scan Listeners - this.socket.on('scan_start', this.scanStart) - this.socket.on('scan_complete', this.scanComplete) - this.socket.on('scan_progress', this.scanProgress) - // Task Listeners this.socket.on('task_started', this.taskStarted) this.socket.on('task_finished', this.taskFinished) diff --git a/client/store/scanners.js b/client/store/scanners.js index de154c3d5..ccdc1791b 100644 --- a/client/store/scanners.js +++ b/client/store/scanners.js @@ -1,5 +1,4 @@ export const state = () => ({ - libraryScans: [], providers: [ { text: 'Google Books', @@ -72,26 +71,8 @@ export const state = () => ({ ] }) -export const getters = { - getLibraryScan: state => id => { - return state.libraryScans.find(ls => ls.id === id) - } -} +export const getters = {} -export const actions = { +export const actions = {} -} - -export const mutations = { - addUpdate(state, data) { - const index = state.libraryScans.findIndex(lib => lib.id === data.id) - if (index >= 0) { - state.libraryScans.splice(index, 1, data) - } else { - state.libraryScans.push(data) - } - }, - remove(state, data) { - state.libraryScans = state.libraryScans.filter(scan => scan.id !== data.id) - } -} \ No newline at end of file +export const mutations = {} \ No newline at end of file diff --git a/client/store/tasks.js b/client/store/tasks.js index 9277d412e..96e7e5b82 100644 --- a/client/store/tasks.js +++ b/client/store/tasks.js @@ -9,7 +9,8 @@ export const getters = { return state.tasks.filter(t => t.data?.libraryItemId === libraryItemId) }, getRunningLibraryScanTask: (state) => (libraryId) => { - return state.tasks.find(t => t.data?.libraryId === libraryId && !t.isFinished) + const libraryScanActions = ['library-scan', 'library-match-all'] + return state.tasks.find(t => libraryScanActions.includes(t.action) && t.data?.libraryId === libraryId && !t.isFinished) } } diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 9c593ff28..10a77b2a4 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -775,6 +775,13 @@ class LibraryController { }) } + /** + * GET: /api/libraries/:id/matchall + * Quick match all library items. Book libraries only. + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ async matchAll(req, res) { if (!req.user.isAdminOrUp) { Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index d17dbd153..b6f2f2858 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -69,7 +69,7 @@ class LibraryItem extends Model { * * @param {number} offset * @param {number} limit - * @returns {Promise<Model<LibraryItem>[]>} LibraryItem + * @returns {Promise<LibraryItem[]>} LibraryItem */ static getLibraryItemsIncrement(offset, limit, where = null) { return this.findAll({ diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index ebce36073..616baf295 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -12,6 +12,7 @@ const Author = require('../objects/entities/Author') const Series = require('../objects/entities/Series') const LibraryScanner = require('./LibraryScanner') const CoverManager = require('../managers/CoverManager') +const TaskManager = require('../managers/TaskManager') class Scanner { constructor() { } @@ -280,6 +281,14 @@ class Scanner { return false } + /** + * Quick match library items + * + * @param {import('../objects/Library')} library + * @param {import('../objects/LibraryItem')[]} libraryItems + * @param {LibraryScan} libraryScan + * @returns {Promise<boolean>} false if scan canceled + */ async matchLibraryItemsChunk(library, libraryItems, libraryScan) { for (let i = 0; i < libraryItems.length; i++) { const libraryItem = libraryItems[i] @@ -313,6 +322,11 @@ class Scanner { return true } + /** + * Quick match all library items for library + * + * @param {import('../objects/Library')} library + */ async matchLibraryItems(library) { if (library.mediaType === 'podcast') { Logger.error(`[Scanner] matchLibraryItems: Match all not supported for podcasts yet`) @@ -330,11 +344,14 @@ class Scanner { const libraryScan = new LibraryScan() libraryScan.setData(library, 'match') LibraryScanner.librariesScanning.push(libraryScan.getScanEmitData) - SocketAuthority.emitter('scan_start', libraryScan.getScanEmitData) - + const taskData = { + libraryId: library.id + } + const task = TaskManager.createAndAddTask('library-match-all', `Matching books in "${library.name}"`, null, true, taskData) Logger.info(`[Scanner] matchLibraryItems: Starting library match scan ${libraryScan.id} for ${libraryScan.libraryName}`) let hasMoreChunks = true + let isCanceled = false while (hasMoreChunks) { const libraryItems = await Database.libraryItemModel.getLibraryItemsIncrement(offset, limit, { libraryId: library.id }) if (!libraryItems.length) { @@ -347,6 +364,7 @@ class Scanner { const shouldContinue = await this.matchLibraryItemsChunk(library, oldLibraryItems, libraryScan) if (!shouldContinue) { + isCanceled = true break } } @@ -354,13 +372,15 @@ class Scanner { if (offset === 0) { Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`) libraryScan.setComplete('Library has no items') + task.setFailed(libraryScan.error) } else { libraryScan.setComplete() + task.setFinished(isCanceled ? 'Canceled' : libraryScan.scanResultsString) } delete LibraryScanner.cancelLibraryScan[libraryScan.libraryId] LibraryScanner.librariesScanning = LibraryScanner.librariesScanning.filter(ls => ls.id !== library.id) - SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData) + TaskManager.taskFinished(task) } } module.exports = new Scanner() From 8ecec93e670d31e100c38188ef297d79ed5cf35f Mon Sep 17 00:00:00 2001 From: Hallo951 <40667862+Hallo951@users.noreply.github.com> Date: Sat, 21 Oct 2023 22:33:31 +0200 Subject: [PATCH 077/208] Update de.json --- client/strings/de.json | 56 +++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index 7bcc2df99..67dc2930c 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -218,7 +218,7 @@ "LabelCurrently": "Aktuell:", "LabelCustomCronExpression": "Benutzerdefinierter Cron-Ausdruck", "LabelDatetime": "Datum & Uhrzeit", - "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", + "LabelDeleteFromFileSystemCheckbox": "Löschen von der Festplatte + Datenbank (deaktivieren um nur aus der Datenbank zu löschen)", "LabelDescription": "Beschreibung", "LabelDeselectAll": "Alles abwählen", "LabelDevice": "Gerät", @@ -519,34 +519,34 @@ "MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)", "MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)", "MessageCheckingCron": "Überprüfe Cron...", - "MessageConfirmCloseFeed": "Sind Sie sicher, dass Sie diesen Feed schließen wollen?", - "MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?", - "MessageConfirmDeleteFile": "Es wird die Datei vom System löschen. Sind Sie sicher?", - "MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?", - "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", - "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", - "MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?", - "MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?", - "MessageConfirmMarkAllEpisodesFinished": "Sind Sie sicher, dass Sie alle Episoden als abgeschlossen markieren möchten?", - "MessageConfirmMarkAllEpisodesNotFinished": "Sind Sie sicher, dass Sie alle Episoden als nicht abgeschlossen markieren möchten?", - "MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?", - "MessageConfirmMarkSeriesNotFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als nicht abgeschlossen markieren wollen?", - "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", - "MessageConfirmRemoveAllChapters": "Sind Sie sicher, dass Sie alle Kapitel entfernen möchten?", - "MessageConfirmRemoveAuthor": "Sind Sie sicher, dass Sie den Autor \"{0}\" enfernen möchten?", - "MessageConfirmRemoveCollection": "Sind Sie sicher, dass Sie die Sammlung \"{0}\" löschen wollen?", - "MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?", - "MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?", - "MessageConfirmRemoveNarrator": "Sind Sie sicher, dass Sie den Erzähler \"{0}\" löschen möchten?", - "MessageConfirmRemovePlaylist": "Sind Sie sicher, dass Sie die Wiedergabeliste \"{0}\" entfernen möchten?", - "MessageConfirmRenameGenre": "Sind Sie sicher, dass Sie die Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?", + "MessageConfirmCloseFeed": "Feed wird geschlossen! Sind Sie sicher?", + "MessageConfirmDeleteBackup": "Sicherung für {0} wird gelöscht! Sind Sie sicher?", + "MessageConfirmDeleteFile": "Datei wird vom System gelöscht! Sind Sie sicher?", + "MessageConfirmDeleteLibrary": "Bibliothek \"{0}\" wird dauerhaft gelöscht! Sind Sie sicher?", + "MessageConfirmDeleteLibraryItem": "Bibliothekselement wird aus der Datenbank + Festplatte gelöscht? Sind Sie sicher?", + "MessageConfirmDeleteLibraryItems": "{0} Bibliothekselemente werden aus der Datenbank + Festplatte gelöscht? Sind Sie sicher?", + "MessageConfirmDeleteSession": "Sitzung wird gelöscht! Sind Sie sicher?", + "MessageConfirmForceReScan": "Scanvorgang erzwingen! Sind Sie sicher?", + "MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Sind Sie sicher?", + "MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Sind Sie sicher?", + "MessageConfirmMarkSeriesFinished": "Alle Medien dieser Reihe werden als abgeschlossen markiert! Sind Sie sicher?", + "MessageConfirmMarkSeriesNotFinished": "Alle Medien dieser Reihe werden als nicht abgeschlossen markiert! Sind Sie sicher?", + "MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achten Sie darauf, dass Sie eine Sicherungskopie der Audiodateien besitzen. <br><br>Möchten Sie fortfahren?", + "MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Sind Sie sicher?", + "MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Sind Sie sicher?", + "MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird gelöscht! Sind Sie sicher?", + "MessageConfirmRemoveEpisode": "Episode \"{0}\" wird geloscht! Sind Sie sicher?", + "MessageConfirmRemoveEpisodes": "{0} Episoden werden gelöscht! Sind Sie sicher?", + "MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird gelöscht! Sind Sie sicher?", + "MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Sind Sie sicher?", + "MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Sind Sie sicher?", "MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.", "MessageConfirmRenameGenreWarning": "Warnung! Ein ähnliche Kategorie mit einem anderen Wortlaut existiert bereits: \"{0}\".", - "MessageConfirmRenameTag": "Sind Sie sicher, dass Sie den Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?", + "MessageConfirmRenameTag": "Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Sind Sie sicher?", "MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.", "MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".", - "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", - "MessageConfirmSendEbookToDevice": "Sind Sie sicher, dass sie {0} ebook \"{1}\" auf das Gerät \"{2}\" senden wollen?", + "MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Sind Sie sicher?", + "MessageConfirmSendEbookToDevice": "{0} E-Book \"{1}\" werden auf das Gerät \"{2}\" gesendet! Sind Sie sicher?", "MessageDownloadingEpisode": "Episode herunterladen", "MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge", "MessageEmbedFinished": "Einbettung abgeschlossen!", @@ -610,9 +610,9 @@ "MessageRemoveChapter": "Kapitel löschen", "MessageRemoveEpisodes": "Entferne {0} Episode(n)", "MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen", - "MessageRemoveUserWarning": "Sind Sie sicher, dass Sie den Benutzer \"{0}\" dauerhaft löschen wollen?", + "MessageRemoveUserWarning": "Benutzer \"{0}\" wird dauerhaft gelöscht! Sind Sie sicher?", "MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf", - "MessageResetChaptersConfirm": "Sind Sie sicher, dass Sie die Kapitel zurücksetzen und die vorgenommenen Änderungen rückgängig machen wollen?", + "MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Sind Sie sicher?", "MessageRestoreBackupConfirm": "Sind Sie sicher, dass Sie die Sicherung wiederherstellen wollen, welche am", "MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in Ihren Bibliotheksordnern verändert. Wenn Sie die Servereinstellungen aktiviert haben, um Cover und Metadaten in Ihren Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.", "MessageSearchResultsFor": "Suchergebnisse für", @@ -713,4 +713,4 @@ "ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen", "ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden", "ToastUserDeleteSuccess": "Benutzer gelöscht" -} \ No newline at end of file +} From b42edfe7a726ed78f6cd2e4422f09d92183dcd05 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 22 Oct 2023 07:10:52 -0500 Subject: [PATCH 078/208] Book duration shown on match page compares minutes #1803 --- client/components/cards/BookMatchCard.vue | 16 ++++++++-------- client/pages/config/index.vue | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/components/cards/BookMatchCard.vue b/client/components/cards/BookMatchCard.vue index 77619e550..f2c15280e 100644 --- a/client/components/cards/BookMatchCard.vue +++ b/client/components/cards/BookMatchCard.vue @@ -70,14 +70,14 @@ export default { return (this.book.duration || 0) * 60 }, bookDurationComparison() { - if (!this.bookDuration || !this.currentBookDuration) return '' - let differenceInSeconds = this.currentBookDuration - this.bookDuration - // Only show seconds on difference if difference is less than an hour - if (differenceInSeconds < 0) { - differenceInSeconds = Math.abs(differenceInSeconds) - return `(${this.$elapsedPrettyExtended(differenceInSeconds, false, differenceInSeconds < 3600)} shorter)` - } else if (differenceInSeconds > 0) { - return `(${this.$elapsedPrettyExtended(differenceInSeconds, false, differenceInSeconds < 3600)} longer)` + if (!this.book.duration || !this.currentBookDuration) return '' + const currentBookDurationMinutes = Math.floor(this.currentBookDuration / 60) + let differenceInMinutes = currentBookDurationMinutes - this.book.duration + if (differenceInMinutes < 0) { + differenceInMinutes = Math.abs(differenceInMinutes) + return `(${this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)} shorter)` + } else if (differenceInMinutes > 0) { + return `(${this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)} longer)` } return '(exact match)' } diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 936f6a303..1721a3796 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -244,7 +244,7 @@ export default { value: 'json' }, { - text: '.abs', + text: '.abs (deprecated)', value: 'abs' } ] From ce88c6ccc37362cda4a8c9b79091169b894eaa26 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 22 Oct 2023 12:58:05 -0500 Subject: [PATCH 079/208] Scanner metadata order of precedence description label, link to guide, add translations --- .../modals/libraries/LibraryScannerSettings.vue | 15 ++++++++++++--- client/strings/da.json | 4 ++++ client/strings/de.json | 6 +++++- client/strings/en-us.json | 4 ++++ client/strings/es.json | 4 ++++ client/strings/fr.json | 4 ++++ client/strings/gu.json | 4 ++++ client/strings/hi.json | 4 ++++ client/strings/hr.json | 4 ++++ client/strings/it.json | 4 ++++ client/strings/lt.json | 4 ++++ client/strings/nl.json | 4 ++++ client/strings/no.json | 4 ++++ client/strings/pl.json | 4 ++++ client/strings/ru.json | 4 ++++ client/strings/zh-cn.json | 4 ++++ 16 files changed, 73 insertions(+), 4 deletions(-) diff --git a/client/components/modals/libraries/LibraryScannerSettings.vue b/client/components/modals/libraries/LibraryScannerSettings.vue index 95ae801a7..215f79b55 100644 --- a/client/components/modals/libraries/LibraryScannerSettings.vue +++ b/client/components/modals/libraries/LibraryScannerSettings.vue @@ -1,8 +1,17 @@ <template> <div class="w-full h-full px-1 md:px-4 py-1 mb-4"> - <div class="flex items-center justify-between mb-4"> - <h2 class="text-lg text-gray-200">Metadata order of precedence</h2> - <ui-btn small @click="resetToDefault">Reset to default</ui-btn> + <div class="flex items-center justify-between mb-2"> + <h2 class="text-base md:text-lg text-gray-200">{{ $strings.HeaderMetadataOrderOfPrecedence }}</h2> + <ui-btn small @click="resetToDefault">{{ $strings.ButtonResetToDefault }}</ui-btn> + </div> + + <div class="flex items-center justify-between md:justify-start mb-4"> + <p class="text-sm text-gray-300 pr-2">{{ $strings.LabelMetadataOrderOfPrecedenceDescription }}</p> + <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex"> + <a href="https://www.audiobookshelf.org/guides/book-scanner" target="_blank" class="inline-flex"> + <span class="material-icons text-xl w-5">help_outline</span> + </a> + </ui-tooltip> </div> <draggable v-model="metadataSourceMapped" v-bind="dragOptions" class="list-group" draggable=".item" handle=".drag-handle" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate"> diff --git a/client/strings/da.json b/client/strings/da.json index 905beb262..aa1b66ed8 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Fjern Serie fra Fortsæt Serie", "ButtonReScan": "Gen-scan", "ButtonReset": "Nulstil", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Gendan", "ButtonSave": "Gem", "ButtonSaveAndClose": "Gem & Luk", @@ -122,6 +123,7 @@ "HeaderManageTags": "Administrer Tags", "HeaderMapDetails": "Kort Detaljer", "HeaderMatch": "Match", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Metadata til indlejring", "HeaderNewAccount": "Ny Konto", "HeaderNewLibrary": "Nyt Bibliotek", @@ -200,6 +202,7 @@ "LabelChapters": "Kapitler", "LabelChaptersFound": "fundne kapitler", "LabelChapterTitle": "Kapitel Titel", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Luk afspiller", "LabelCodec": "Codec", "LabelCollapseSeries": "Fold Serie Sammen", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato", "LabelMediaPlayer": "Medieafspiller", "LabelMediaType": "Medietype", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Metadataudbyder", "LabelMetaTag": "Meta-tag", "LabelMetaTags": "Meta-tags", diff --git a/client/strings/de.json b/client/strings/de.json index 67dc2930c..fe1df71c2 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste", "ButtonReScan": "Neu scannen", "ButtonReset": "Zurücksetzen", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Wiederherstellen", "ButtonSave": "Speichern", "ButtonSaveAndClose": "Speichern & Schließen", @@ -122,6 +123,7 @@ "HeaderManageTags": "Tags verwalten", "HeaderMapDetails": "Stapelverarbeitung", "HeaderMatch": "Metadaten", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Einzubettende Metadaten", "HeaderNewAccount": "Neues Konto", "HeaderNewLibrary": "Neue Bibliothek", @@ -200,6 +202,7 @@ "LabelChapters": "Kapitel", "LabelChaptersFound": "gefundene Kapitel", "LabelChapterTitle": "Kapitelüberschrift", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Player schließen", "LabelCodec": "Codec", "LabelCollapseSeries": "Serien zusammenfassen", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum", "LabelMediaPlayer": "Mediaplayer", "LabelMediaType": "Medientyp", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Metadatenanbieter", "LabelMetaTag": "Meta Schlagwort", "LabelMetaTags": "Meta Tags", @@ -713,4 +717,4 @@ "ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen", "ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden", "ToastUserDeleteSuccess": "Benutzer gelöscht" -} +} \ No newline at end of file diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 6befd10d8..43f44821b 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Remove Series from Continue Series", "ButtonReScan": "Re-Scan", "ButtonReset": "Reset", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Restore", "ButtonSave": "Save", "ButtonSaveAndClose": "Save & Close", @@ -122,6 +123,7 @@ "HeaderManageTags": "Manage Tags", "HeaderMapDetails": "Map details", "HeaderMatch": "Match", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Metadata to embed", "HeaderNewAccount": "New Account", "HeaderNewLibrary": "New Library", @@ -200,6 +202,7 @@ "LabelChapters": "Chapters", "LabelChaptersFound": "chapters found", "LabelChapterTitle": "Chapter Title", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Close player", "LabelCodec": "Codec", "LabelCollapseSeries": "Collapse Series", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/es.json b/client/strings/es.json index 57d09a724..3a6012b62 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Remover Serie de Continuar Series", "ButtonReScan": "Re-Escanear", "ButtonReset": "Reiniciar", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Restaurar", "ButtonSave": "Guardar", "ButtonSaveAndClose": "Guardar y Cerrar", @@ -122,6 +123,7 @@ "HeaderManageTags": "Administrar Etiquetas", "HeaderMapDetails": "Asignar Detalles", "HeaderMatch": "Encontrar", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Metadatos para Insertar", "HeaderNewAccount": "Nueva Cuenta", "HeaderNewLibrary": "Nueva Biblioteca", @@ -200,6 +202,7 @@ "LabelChapters": "Capítulos", "LabelChaptersFound": "Capítulo Encontrado", "LabelChapterTitle": "Titulo del Capítulo", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Cerrar Reproductor", "LabelCodec": "Codec", "LabelCollapseSeries": "Colapsar Serie", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha", "LabelMediaPlayer": "Reproductor de Medios", "LabelMediaType": "Tipo de Multimedia", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Proveedor de Metadata", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/fr.json b/client/strings/fr.json index 25b3261ef..a78f7d665 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série", "ButtonReScan": "Nouvelle analyse", "ButtonReset": "Réinitialiser", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Rétablir", "ButtonSave": "Sauvegarder", "ButtonSaveAndClose": "Sauvegarder et Fermer", @@ -122,6 +123,7 @@ "HeaderManageTags": "Gérer les étiquettes", "HeaderMapDetails": "Édition en masse", "HeaderMatch": "Chercher", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Métadonnée à intégrer", "HeaderNewAccount": "Nouveau compte", "HeaderNewLibrary": "Nouvelle bibliothèque", @@ -200,6 +202,7 @@ "LabelChapters": "Chapitres", "LabelChaptersFound": "Chapitres trouvés", "LabelChapterTitle": "Titres du chapitre", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Fermer le lecteur", "LabelCodec": "Codec", "LabelCollapseSeries": "Réduire les séries", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date", "LabelMediaPlayer": "Lecteur multimédia", "LabelMediaType": "Type de média", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Fournisseur de métadonnées", "LabelMetaTag": "Etiquette de métadonnée", "LabelMetaTags": "Etiquettes de métadonnée", diff --git a/client/strings/gu.json b/client/strings/gu.json index 7716773d8..8b6a963fa 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો", "ButtonReScan": "ફરીથી સ્કેન કરો", "ButtonReset": "રીસેટ કરો", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "પુનઃસ્થાપિત કરો", "ButtonSave": "સાચવો", "ButtonSaveAndClose": "સાચવો અને બંધ કરો", @@ -122,6 +123,7 @@ "HeaderManageTags": "Manage Tags", "HeaderMapDetails": "Map details", "HeaderMatch": "Match", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Metadata to embed", "HeaderNewAccount": "New Account", "HeaderNewLibrary": "New Library", @@ -200,6 +202,7 @@ "LabelChapters": "Chapters", "LabelChaptersFound": "chapters found", "LabelChapterTitle": "Chapter Title", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Close player", "LabelCodec": "Codec", "LabelCollapseSeries": "Collapse Series", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/hi.json b/client/strings/hi.json index 3cc25ae60..7c8651e3f 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "इस सीरीज को कंटिन्यू सीरीज से हटा दें", "ButtonReScan": "पुन: स्कैन करें", "ButtonReset": "रीसेट करें", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "पुनर्स्थापित करें", "ButtonSave": "सहेजें", "ButtonSaveAndClose": "सहेजें और बंद करें", @@ -122,6 +123,7 @@ "HeaderManageTags": "Manage Tags", "HeaderMapDetails": "Map details", "HeaderMatch": "Match", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Metadata to embed", "HeaderNewAccount": "New Account", "HeaderNewLibrary": "New Library", @@ -200,6 +202,7 @@ "LabelChapters": "Chapters", "LabelChaptersFound": "chapters found", "LabelChapterTitle": "Chapter Title", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Close player", "LabelCodec": "Codec", "LabelCollapseSeries": "Collapse Series", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/hr.json b/client/strings/hr.json index 1a97f0f42..8e3946a55 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Ukloni seriju iz Nastavi seriju", "ButtonReScan": "Skeniraj ponovno", "ButtonReset": "Poništi", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Povrati", "ButtonSave": "Spremi", "ButtonSaveAndClose": "Spremi i zatvori", @@ -122,6 +123,7 @@ "HeaderManageTags": "Manage Tags", "HeaderMapDetails": "Map details", "HeaderMatch": "Match", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Metapodatci za ugradnju", "HeaderNewAccount": "Novi korisnički račun", "HeaderNewLibrary": "Nova biblioteka", @@ -200,6 +202,7 @@ "LabelChapters": "Chapters", "LabelChaptersFound": "poglavlja pronađena", "LabelChapterTitle": "Ime poglavlja", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Close player", "LabelCodec": "Codec", "LabelCollapseSeries": "Collapse Series", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Poslužitelj metapodataka ", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/it.json b/client/strings/it.json index 003e167c0..e384ea59a 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla", "ButtonReScan": "Ri-scansiona", "ButtonReset": "Reset", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Ripristina", "ButtonSave": "Salva", "ButtonSaveAndClose": "Salva & Chiudi", @@ -122,6 +123,7 @@ "HeaderManageTags": "Gestisci Tags", "HeaderMapDetails": "Mappa Dettagli", "HeaderMatch": "Trova Corrispondenza", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Metadata da incorporare", "HeaderNewAccount": "Nuovo Account", "HeaderNewLibrary": "Nuova Libreria", @@ -200,6 +202,7 @@ "LabelChapters": "Capitoli", "LabelChaptersFound": "Capitoli Trovati", "LabelChapterTitle": "Titoli dei Capitoli", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Chiudi player", "LabelCodec": "Codec", "LabelCollapseSeries": "Comprimi Serie", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Tipo Media", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/lt.json b/client/strings/lt.json index 3266e978d..c5c937ba4 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Pašalinti seriją iš Tęsti Seriją", "ButtonReScan": "Iš naujo nuskaityti", "ButtonReset": "Atstatyti", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Atkurti", "ButtonSave": "Išsaugoti", "ButtonSaveAndClose": "Išsaugoti ir uždaryti", @@ -122,6 +123,7 @@ "HeaderManageTags": "Tvarkyti žymas", "HeaderMapDetails": "Susieti detales", "HeaderMatch": "Atitaikyti", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Metaduomenys įterpimui", "HeaderNewAccount": "Nauja paskyra", "HeaderNewLibrary": "Nauja biblioteka", @@ -200,6 +202,7 @@ "LabelChapters": "Skyriai", "LabelChaptersFound": "rasti skyriai", "LabelChapterTitle": "Skyriaus pavadinimas", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Uždaryti grotuvą", "LabelCodec": "Kodekas", "LabelCollapseSeries": "Suskleisti seriją", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Ieškoti naujų epizodų po šios datos", "LabelMediaPlayer": "Grotuvas", "LabelMediaType": "Medijos tipas", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Metaduomenų tiekėjas", "LabelMetaTag": "Meta žymė", "LabelMetaTags": "Meta žymos", diff --git a/client/strings/nl.json b/client/strings/nl.json index da0b80460..a2046d576 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen", "ButtonReScan": "Nieuwe scan", "ButtonReset": "Reset", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Herstel", "ButtonSave": "Opslaan", "ButtonSaveAndClose": "Opslaan & sluiten", @@ -122,6 +123,7 @@ "HeaderManageTags": "Tags beheren", "HeaderMapDetails": "Map details", "HeaderMatch": "Match", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "In te sluiten metadata", "HeaderNewAccount": "Nieuwe account", "HeaderNewLibrary": "Nieuwe bibliotheek", @@ -200,6 +202,7 @@ "LabelChapters": "Hoofdstukken", "LabelChaptersFound": "Hoofdstukken gevonden", "LabelChapterTitle": "Hoofdstuktitel", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Sluit speler", "LabelCodec": "Codec", "LabelCollapseSeries": "Series inklappen", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum", "LabelMediaPlayer": "Mediaspeler", "LabelMediaType": "Mediatype", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Metadatabron", "LabelMetaTag": "Meta-tag", "LabelMetaTags": "Meta-tags", diff --git a/client/strings/no.json b/client/strings/no.json index 90e8758f3..26a282af3 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Fjern serie fra Fortsett serie", "ButtonReScan": "Skann på nytt", "ButtonReset": "Nullstill", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Gjenopprett", "ButtonSave": "Lagre", "ButtonSaveAndClose": "Lagre og lukk", @@ -122,6 +123,7 @@ "HeaderManageTags": "Behandle tags", "HeaderMapDetails": "Kartleggingsdetaljer", "HeaderMatch": "Tilpasse", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Metadata å bake inn", "HeaderNewAccount": "Ny konto", "HeaderNewLibrary": "Ny bibliotek", @@ -200,6 +202,7 @@ "LabelChapters": "Kapitler", "LabelChaptersFound": "kapitler funnet", "LabelChapterTitle": "Kapittel tittel", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Lukk spiller", "LabelCodec": "Kodek", "LabelCollapseSeries": "Minimer serier", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Se etter nye episoder etter denne datoen", "LabelMediaPlayer": "Mediespiller", "LabelMediaType": "Medie type", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Metadata Leverandør", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/pl.json b/client/strings/pl.json index 82167c081..b38406baa 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Usuń serię z listy odtwarzania", "ButtonReScan": "Ponowne skanowanie", "ButtonReset": "Resetowanie", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Przywróć", "ButtonSave": "Zapisz", "ButtonSaveAndClose": "Zapisz i zamknij", @@ -122,6 +123,7 @@ "HeaderManageTags": "Manage Tags", "HeaderMapDetails": "Map details", "HeaderMatch": "Dopasuj", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Osadź metadane", "HeaderNewAccount": "Nowe konto", "HeaderNewLibrary": "Nowa biblioteka", @@ -200,6 +202,7 @@ "LabelChapters": "Chapters", "LabelChaptersFound": "Znalezione rozdziały", "LabelChapterTitle": "Tytuł rozdziału", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Zamknij odtwarzacz", "LabelCodec": "Codec", "LabelCollapseSeries": "Podsumuj serię", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie", "LabelMediaPlayer": "Odtwarzacz", "LabelMediaType": "Typ mediów", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Dostawca metadanych", "LabelMetaTag": "Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/ru.json b/client/strings/ru.json index d4d258d31..94a8bc63f 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Удалить серию из Продолжить серию", "ButtonReScan": "Пересканировать", "ButtonReset": "Сбросить", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "Восстановить", "ButtonSave": "Сохранить", "ButtonSaveAndClose": "Сохранить и закрыть", @@ -122,6 +123,7 @@ "HeaderManageTags": "Редактировать теги", "HeaderMapDetails": "Найти подробности", "HeaderMatch": "Поиск", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "Метаинформация для встраивания", "HeaderNewAccount": "Новая учетная запись", "HeaderNewLibrary": "Новая библиотека", @@ -200,6 +202,7 @@ "LabelChapters": "Главы", "LabelChaptersFound": "глав найдено", "LabelChapterTitle": "Название главы", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "Закрыть проигрыватель", "LabelCodec": "Кодек", "LabelCollapseSeries": "Свернуть серии", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты", "LabelMediaPlayer": "Медиа проигрыватель", "LabelMediaType": "Тип медиа", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "Провайдер", "LabelMetaTag": "Мета тег", "LabelMetaTags": "Мета теги", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index b76e79494..0ea26727e 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -59,6 +59,7 @@ "ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除", "ButtonReScan": "重新扫描", "ButtonReset": "重置", + "ButtonResetToDefault": "Reset to default", "ButtonRestore": "恢复", "ButtonSave": "保存", "ButtonSaveAndClose": "保存并关闭", @@ -122,6 +123,7 @@ "HeaderManageTags": "管理标签", "HeaderMapDetails": "编辑详情", "HeaderMatch": "匹配", + "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataToEmbed": "嵌入元数据", "HeaderNewAccount": "新建帐户", "HeaderNewLibrary": "新建媒体库", @@ -200,6 +202,7 @@ "LabelChapters": "章节", "LabelChaptersFound": "找到的章节", "LabelChapterTitle": "章节标题", + "LabelClickForMoreInfo": "Click for more info", "LabelClosePlayer": "关闭播放器", "LabelCodec": "编解码", "LabelCollapseSeries": "折叠系列", @@ -307,6 +310,7 @@ "LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集", "LabelMediaPlayer": "媒体播放器", "LabelMediaType": "媒体类型", + "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", "LabelMetadataProvider": "元数据提供者", "LabelMetaTag": "元数据标签", "LabelMetaTags": "元标签", From 60a80a2996895373c797f5b119204f6492274470 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 22 Oct 2023 15:53:05 -0500 Subject: [PATCH 080/208] Update:Remove support for metadata.abs, added script to create metadata.json files if they dont exist --- client/package.json | 1 + client/pages/config/index.vue | 20 +- client/strings/da.json | 2 +- client/strings/de.json | 2 +- client/strings/en-us.json | 2 +- client/strings/es.json | 2 +- client/strings/fr.json | 2 +- client/strings/gu.json | 2 +- client/strings/hi.json | 2 +- client/strings/hr.json | 2 +- client/strings/it.json | 2 +- client/strings/lt.json | 2 +- client/strings/nl.json | 2 +- client/strings/no.json | 2 +- client/strings/pl.json | 2 +- client/strings/ru.json | 2 +- client/strings/zh-cn.json | 2 +- package.json | 3 +- server/Database.js | 24 +- server/models/Book.js | 26 + server/models/Podcast.js | 19 + server/objects/LibraryItem.js | 117 ++-- server/objects/mediaTypes/Book.js | 2 +- server/objects/mediaTypes/Podcast.js | 14 +- server/objects/settings/ServerSettings.js | 14 +- server/scanner/AbsMetadataFileScanner.js | 83 +-- server/scanner/BookScanner.js | 175 ++---- server/scanner/PodcastScanner.js | 161 ++--- .../utils/generators/abmetadataGenerator.js | 553 +----------------- .../utils/migrations/absMetadataMigration.js | 93 +++ 30 files changed, 390 insertions(+), 945 deletions(-) create mode 100644 server/utils/migrations/absMetadataMigration.js diff --git a/client/package.json b/client/package.json index cc7859269..21cae1248 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,7 @@ { "name": "audiobookshelf-client", "version": "2.4.4", + "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", "scripts": { diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 1721a3796..12ce7b1e6 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -47,10 +47,6 @@ <p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p> </div> - <div class="w-44 mb-2"> - <ui-dropdown v-model="newServerSettings.metadataFileFormat" small :items="metadataFileFormats" label="Metadata File Format" @input="updateMetadataFileFormat" :disabled="updatingServerSettings" /> - </div> - <div class="pt-4"> <h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2> </div> @@ -237,17 +233,7 @@ export default { hasPrefixesChanged: false, newServerSettings: {}, showConfirmPurgeCache: false, - savingPrefixes: false, - metadataFileFormats: [ - { - text: '.json', - value: 'json' - }, - { - text: '.abs (deprecated)', - value: 'abs' - } - ] + savingPrefixes: false } }, watch: { @@ -329,10 +315,6 @@ export default { updateServerLanguage(val) { this.updateSettingsKey('language', val) }, - updateMetadataFileFormat(val) { - if (this.serverSettings.metadataFileFormat === val) return - this.updateSettingsKey('metadataFileFormat', val) - }, updateSettingsKey(key, val) { if (key === 'scannerDisableWatcher') { this.newServerSettings.scannerDisableWatcher = val diff --git a/client/strings/da.json b/client/strings/da.json index aa1b66ed8..adf138a1c 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Gem omslag med element", "LabelSettingsStoreCoversWithItemHelp": "Som standard gemmes omslag i /metadata/items, aktivering af denne indstilling vil gemme omslag i mappen for dit bibliotekselement. Kun én fil med navnet \"cover\" vil blive bevaret", "LabelSettingsStoreMetadataWithItem": "Gem metadata med element", - "LabelSettingsStoreMetadataWithItemHelp": "Som standard gemmes metadatafiler i /metadata/items, aktivering af denne indstilling vil gemme metadatafiler i dine bibliotekselementmapper. Bruger .abs-filudvidelsen", + "LabelSettingsStoreMetadataWithItemHelp": "Som standard gemmes metadatafiler i /metadata/items, aktivering af denne indstilling vil gemme metadatafiler i dine bibliotekselementmapper", "LabelSettingsTimeFormat": "Tidsformat", "LabelShowAll": "Vis alle", "LabelSize": "Størrelse", diff --git a/client/strings/de.json b/client/strings/de.json index fe1df71c2..a072a549f 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Titelbilder im Medienordner speichern", "LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.", "LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern", - "LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.", + "LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet", "LabelSettingsTimeFormat": "Zeitformat", "LabelShowAll": "Alles anzeigen", "LabelSize": "Größe", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 43f44821b..24d077263 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Store covers with item", "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept", "LabelSettingsStoreMetadataWithItem": "Store metadata with item", - "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension", + "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders", "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Show All", "LabelSize": "Size", diff --git a/client/strings/es.json b/client/strings/es.json index 3a6012b62..4b37139db 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Guardar portadas con elementos", "LabelSettingsStoreCoversWithItemHelp": "Por defecto, las portadas se almacenan en /metadata/items. Si habilita esta opción, las portadas se almacenarán en la carpeta de elementos de su biblioteca. Se guardará un solo archivo llamado \"cover\".", "LabelSettingsStoreMetadataWithItem": "Guardar metadatos con elementos", - "LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca. Usa la extensión .abs", + "LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca", "LabelSettingsTimeFormat": "Formato de Tiempo", "LabelShowAll": "Mostrar Todos", "LabelSize": "Tamaño", diff --git a/client/strings/fr.json b/client/strings/fr.json index a78f7d665..28bdf743f 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les articles", "LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de l’article. Seul un fichier nommé « cover » sera conservé.", "LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles", - "LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les métadonnées dans le dossier de l’article avec une extension « .abs ».", + "LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items", "LabelSettingsTimeFormat": "Format d’heure", "LabelShowAll": "Afficher Tout", "LabelSize": "Taille", diff --git a/client/strings/gu.json b/client/strings/gu.json index 8b6a963fa..8593a95d8 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Store covers with item", "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept", "LabelSettingsStoreMetadataWithItem": "Store metadata with item", - "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension", + "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders", "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Show All", "LabelSize": "Size", diff --git a/client/strings/hi.json b/client/strings/hi.json index 7c8651e3f..82d25986c 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Store covers with item", "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept", "LabelSettingsStoreMetadataWithItem": "Store metadata with item", - "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension", + "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders", "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Show All", "LabelSize": "Size", diff --git a/client/strings/hr.json b/client/strings/hr.json index 8e3946a55..e9a323eee 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Spremi cover uz stakvu", "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept", "LabelSettingsStoreMetadataWithItem": "Spremi metapodatke uz stavku", - "LabelSettingsStoreMetadataWithItemHelp": "Po defaultu metapodatci su spremljeni u /metadata/items, uključujućite li ovu postavku, metapodatci će biti spremljeni u folderima od biblioteke. Koristi .abs ekstenziju.", + "LabelSettingsStoreMetadataWithItemHelp": "Po defaultu metapodatci su spremljeni u /metadata/items, uključujućite li ovu postavku, metapodatci će biti spremljeni u folderima od biblioteke", "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Prikaži sve", "LabelSize": "Veličina", diff --git a/client/strings/it.json b/client/strings/it.json index e384ea59a..f73b3ffc6 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Archivia le copertine con il file", "LabelSettingsStoreCoversWithItemHelp": "Di default, le immagini di copertina sono salvate dentro /metadata/items, abilitando questa opzione le copertine saranno archiviate nella cartella della libreria corrispondente. Verrà conservato solo un file denominato \"cover\"", "LabelSettingsStoreMetadataWithItem": "Archivia i metadata con il file", - "LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria. I file avranno estensione .abs", + "LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria", "LabelSettingsTimeFormat": "Formato Ora", "LabelShowAll": "Mostra Tutto", "LabelSize": "Dimensione", diff --git a/client/strings/lt.json b/client/strings/lt.json index c5c937ba4..dee54e124 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Saugoti viršelius su elementu", "LabelSettingsStoreCoversWithItemHelp": "Pagal nutylėjimą viršeliai saugomi /metadata/items aplanke, įjungus šią parinktį viršeliai bus saugomi jūsų bibliotekos elemento aplanke. Bus išsaugotas tik vienas „cover“ pavadinimo failas.", "LabelSettingsStoreMetadataWithItem": "Saugoti metaduomenis su elementu", - "LabelSettingsStoreMetadataWithItemHelp": "Pagal nutylėjimą metaduomenų failai saugomi /metadata/items aplanke, įjungus šią parinktį metaduomenų failai bus saugomi jūsų bibliotekos elemento aplanke. Naudojamas .abs plėtinys.", + "LabelSettingsStoreMetadataWithItemHelp": "Pagal nutylėjimą metaduomenų failai saugomi /metadata/items aplanke, įjungus šią parinktį metaduomenų failai bus saugomi jūsų bibliotekos elemento aplanke", "LabelSettingsTimeFormat": "Laiko formatas", "LabelShowAll": "Rodyti viską", "LabelSize": "Dydis", diff --git a/client/strings/nl.json b/client/strings/nl.json index a2046d576..62696dce1 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Bewaar covers bij onderdeel", "LabelSettingsStoreCoversWithItemHelp": "Standaard worden covers bewaard in /metadata/items, door deze instelling in te schakelen zullen covers in de map van je bibliotheekonderdeel bewaard worden. Slechts een bestand genaamd \"cover\" zal worden bewaard", "LabelSettingsStoreMetadataWithItem": "Bewaar metadata bij onderdeel", - "LabelSettingsStoreMetadataWithItemHelp": "Standaard worden metadata-bestanden bewaard in /metadata/items, door deze instelling in te schakelen zullen metadata bestanden in de map van je bibliotheekonderdeel bewaard worden. Gebruikt .abs-extensie", + "LabelSettingsStoreMetadataWithItemHelp": "Standaard worden metadata-bestanden bewaard in /metadata/items, door deze instelling in te schakelen zullen metadata bestanden in de map van je bibliotheekonderdeel bewaard worden", "LabelSettingsTimeFormat": "Tijdformat", "LabelShowAll": "Toon alle", "LabelSize": "Grootte", diff --git a/client/strings/no.json b/client/strings/no.json index 26a282af3..dc7685ee7 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Lagre bokomslag med gjenstand", "LabelSettingsStoreCoversWithItemHelp": "Som standard vil bokomslag bli lagret under /metadata/items, aktiveres dette valget vil bokomslag bli lagret i samme mappe som gjenstanden. Kun en fil med navn \"cover\" vil bli beholdt", "LabelSettingsStoreMetadataWithItem": "Lagre metadata med gjenstand", - "LabelSettingsStoreMetadataWithItemHelp": "Som standard vil metadata bli lagret under /metadata/items, aktiveres dette valget vil metadata bli lagret i samme mappe som gjenstanden. Bruker .abs filetternavn", + "LabelSettingsStoreMetadataWithItemHelp": "Som standard vil metadata bli lagret under /metadata/items, aktiveres dette valget vil metadata bli lagret i samme mappe som gjenstanden", "LabelSettingsTimeFormat": "Tid format", "LabelShowAll": "Vis alt", "LabelSize": "Størrelse", diff --git a/client/strings/pl.json b/client/strings/pl.json index b38406baa..c4fb50f8b 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Przechowuj okładkę w folderze książki", "LabelSettingsStoreCoversWithItemHelp": "Domyślnie okładki są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana.", "LabelSettingsStoreMetadataWithItem": "Przechowuj metadane w folderze książki", - "LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana. Rozszerzenie pliku metadanych: .abs", + "LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana", "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Pokaż wszystko", "LabelSize": "Rozmiar", diff --git a/client/strings/ru.json b/client/strings/ru.json index 94a8bc63f..69868bca1 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "Хранить обложки с элементом", "LabelSettingsStoreCoversWithItemHelp": "По умолчанию обложки сохраняются в папке /metadata/items, при включении этой настройки обложка будет храниться в папке элемента. Будет сохраняться только один файл с именем \"cover\"", "LabelSettingsStoreMetadataWithItem": "Хранить метаинформацию с элементом", - "LabelSettingsStoreMetadataWithItemHelp": "По умолчанию метаинформация сохраняется в папке /metadata/items, при включении этой настройки метаинформация будет храниться в папке элемента. Используется расширение файла .abs", + "LabelSettingsStoreMetadataWithItemHelp": "По умолчанию метаинформация сохраняется в папке /metadata/items, при включении этой настройки метаинформация будет храниться в папке элемента", "LabelSettingsTimeFormat": "Формат времени", "LabelShowAll": "Показать все", "LabelSize": "Размер", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 0ea26727e..219e861ad 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -429,7 +429,7 @@ "LabelSettingsStoreCoversWithItem": "存储项目封面", "LabelSettingsStoreCoversWithItemHelp": "默认情况下封面存储在/metadata/items文件夹中, 启用此设置将存储封面在你媒体项目文件夹中. 只保留一个名为 \"cover\" 的文件", "LabelSettingsStoreMetadataWithItem": "存储项目元数据", - "LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你媒体项目文件夹中. 使 .abs 文件护展名", + "LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你媒体项目文件夹中", "LabelSettingsTimeFormat": "时间格式", "LabelShowAll": "全部显示", "LabelSize": "文件大小", diff --git a/package.json b/package.json index e76147d86..f8ea7deed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "audiobookshelf", "version": "2.4.4", + "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", "scripts": { @@ -45,4 +46,4 @@ "devDependencies": { "nodemon": "^2.0.20" } -} +} \ No newline at end of file diff --git a/server/Database.js b/server/Database.js index 521e016de..5721ac274 100644 --- a/server/Database.js +++ b/server/Database.js @@ -276,11 +276,17 @@ class Database { global.ServerSettings = this.serverSettings.toJSON() // Version specific migrations - if (this.serverSettings.version === '2.3.0' && this.compareVersions(packageJson.version, '2.3.0') == 1) { - await dbMigration.migrationPatch(this) + if (packageJson.version !== this.serverSettings.version) { + if (this.serverSettings.version === '2.3.0' && this.compareVersions(packageJson.version, '2.3.0') == 1) { + await dbMigration.migrationPatch(this) + } + if (['2.3.0', '2.3.1', '2.3.2', '2.3.3'].includes(this.serverSettings.version) && this.compareVersions(packageJson.version, '2.3.3') >= 0) { + await dbMigration.migrationPatch2(this) + } } - if (['2.3.0', '2.3.1', '2.3.2', '2.3.3'].includes(this.serverSettings.version) && this.compareVersions(packageJson.version, '2.3.3') >= 0) { - await dbMigration.migrationPatch2(this) + // Build migrations + if (this.serverSettings.buildNumber <= 0) { + await require('./utils/migrations/absMetadataMigration').migrate(this) } await this.cleanDatabase() @@ -288,9 +294,19 @@ class Database { // Set if root user has been created this.hasRootUser = await this.models.user.getHasRootUser() + // Update server settings with version/build + let updateServerSettings = false if (packageJson.version !== this.serverSettings.version) { Logger.info(`[Database] Server upgrade detected from ${this.serverSettings.version} to ${packageJson.version}`) this.serverSettings.version = packageJson.version + this.serverSettings.buildNumber = packageJson.buildNumber + updateServerSettings = true + } else if (packageJson.buildNumber !== this.serverSettings.buildNumber) { + Logger.info(`[Database] Server v${packageJson.version} build upgraded from ${this.serverSettings.buildNumber} to ${packageJson.buildNumber}`) + this.serverSettings.buildNumber = packageJson.buildNumber + updateServerSettings = true + } + if (updateServerSettings) { await this.updateServerSettings() } } diff --git a/server/models/Book.js b/server/models/Book.js index 31bcfa3c6..9537d7b34 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -211,6 +211,32 @@ class Book extends Model { } } + getAbsMetadataJson() { + return { + tags: this.tags || [], + chapters: this.chapters?.map(c => ({ ...c })) || [], + title: this.title, + subtitle: this.subtitle, + authors: this.authors.map(a => a.name), + narrators: this.narrators, + series: this.series.map(se => { + const sequence = se.bookSeries?.sequence || '' + if (!sequence) return se.name + return `${se.name} #${sequence}` + }), + genres: this.genres || [], + publishedYear: this.publishedYear, + publishedDate: this.publishedDate, + publisher: this.publisher, + description: this.description, + isbn: this.isbn, + asin: this.asin, + language: this.language, + explicit: !!this.explicit, + abridged: !!this.abridged + } + } + /** * Initialize model * @param {import('../Database').sequelize} sequelize diff --git a/server/models/Podcast.js b/server/models/Podcast.js index 60311bfde..82ae8fe2c 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -112,6 +112,25 @@ class Podcast extends Model { } } + getAbsMetadataJson() { + return { + tags: this.tags || [], + title: this.title, + author: this.author, + description: this.description, + releaseDate: this.releaseDate, + genres: this.genres || [], + feedURL: this.feedURL, + imageURL: this.imageURL, + itunesPageURL: this.itunesPageURL, + itunesId: this.itunesId, + itunesArtistId: this.itunesArtistId, + language: this.language, + explicit: !!this.explicit, + podcastType: this.podcastType + } + } + /** * Initialize model * @param {import('../Database').sequelize} sequelize diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js index bb91e2d6d..3b92bdccf 100644 --- a/server/objects/LibraryItem.js +++ b/server/objects/LibraryItem.js @@ -2,7 +2,6 @@ const uuidv4 = require("uuid").v4 const fs = require('../libs/fsExtra') const Path = require('path') const Logger = require('../Logger') -const abmetadataGenerator = require('../utils/generators/abmetadataGenerator') const LibraryFile = require('./files/LibraryFile') const Book = require('./mediaTypes/Book') const Podcast = require('./mediaTypes/Podcast') @@ -263,7 +262,7 @@ class LibraryItem { } /** - * Save metadata.json/metadata.abs file + * Save metadata.json file * TODO: Move to new LibraryItem model * @returns {Promise<LibraryFile>} null if not saved */ @@ -282,91 +281,41 @@ class LibraryItem { await fs.ensureDir(metadataPath) } - const metadataFileFormat = global.ServerSettings.metadataFileFormat - const metadataFilePath = Path.join(metadataPath, `metadata.${metadataFileFormat}`) - if (metadataFileFormat === 'json') { - // Remove metadata.abs if it exists - if (await fs.pathExists(Path.join(metadataPath, `metadata.abs`))) { - Logger.debug(`[LibraryItem] Removing metadata.abs for item "${this.media.metadata.title}"`) - await fs.remove(Path.join(metadataPath, `metadata.abs`)) - this.libraryFiles = this.libraryFiles.filter(lf => lf.metadata.path !== filePathToPOSIX(Path.join(metadataPath, `metadata.abs`))) + const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) + + return fs.writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2)).then(async () => { + // Add metadata.json to libraryFiles array if it is new + let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) + if (storeMetadataWithItem) { + if (!metadataLibraryFile) { + metadataLibraryFile = new LibraryFile() + await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) + this.libraryFiles.push(metadataLibraryFile) + } else { + const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) + if (fileTimestamps) { + metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs + metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs + metadataLibraryFile.metadata.size = fileTimestamps.size + metadataLibraryFile.ino = fileTimestamps.ino + } + } + const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path) + if (libraryItemDirTimestamps) { + this.mtimeMs = libraryItemDirTimestamps.mtimeMs + this.ctimeMs = libraryItemDirTimestamps.ctimeMs + } } - return fs.writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2)).then(async () => { - // Add metadata.json to libraryFiles array if it is new - let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) - if (storeMetadataWithItem) { - if (!metadataLibraryFile) { - metadataLibraryFile = new LibraryFile() - await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) - this.libraryFiles.push(metadataLibraryFile) - } else { - const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) - if (fileTimestamps) { - metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs - metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs - metadataLibraryFile.metadata.size = fileTimestamps.size - metadataLibraryFile.ino = fileTimestamps.ino - } - } - const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path) - if (libraryItemDirTimestamps) { - this.mtimeMs = libraryItemDirTimestamps.mtimeMs - this.ctimeMs = libraryItemDirTimestamps.ctimeMs - } - } + Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`) - Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`) - - return metadataLibraryFile - }).catch((error) => { - Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error) - return null - }).finally(() => { - this.isSavingMetadata = false - }) - } else { - // Remove metadata.json if it exists - if (await fs.pathExists(Path.join(metadataPath, `metadata.json`))) { - Logger.debug(`[LibraryItem] Removing metadata.json for item "${this.media.metadata.title}"`) - await fs.remove(Path.join(metadataPath, `metadata.json`)) - this.libraryFiles = this.libraryFiles.filter(lf => lf.metadata.path !== filePathToPOSIX(Path.join(metadataPath, `metadata.json`))) - } - - return abmetadataGenerator.generate(this, metadataFilePath).then(async (success) => { - if (!success) { - Logger.error(`[LibraryItem] Failed saving abmetadata to "${metadataFilePath}"`) - return null - } - // Add metadata.abs to libraryFiles array if it is new - let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) - if (storeMetadataWithItem) { - if (!metadataLibraryFile) { - metadataLibraryFile = new LibraryFile() - await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.abs`) - this.libraryFiles.push(metadataLibraryFile) - } else { - const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) - if (fileTimestamps) { - metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs - metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs - metadataLibraryFile.metadata.size = fileTimestamps.size - metadataLibraryFile.ino = fileTimestamps.ino - } - } - const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path) - if (libraryItemDirTimestamps) { - this.mtimeMs = libraryItemDirTimestamps.mtimeMs - this.ctimeMs = libraryItemDirTimestamps.ctimeMs - } - } - - Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`) - return metadataLibraryFile - }).finally(() => { - this.isSavingMetadata = false - }) - } + return metadataLibraryFile + }).catch((error) => { + Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error) + return null + }).finally(() => { + this.isSavingMetadata = false + }) } removeLibraryFile(ino) { diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js index afbf1622a..d53a53a72 100644 --- a/server/objects/mediaTypes/Book.js +++ b/server/objects/mediaTypes/Book.js @@ -94,7 +94,7 @@ class Book { return { tags: [...this.tags], chapters: this.chapters.map(c => ({ ...c })), - metadata: this.metadata.toJSONForMetadataFile() + ...this.metadata.toJSONForMetadataFile() } } diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js index 969e25485..a0e5de047 100644 --- a/server/objects/mediaTypes/Podcast.js +++ b/server/objects/mediaTypes/Podcast.js @@ -97,7 +97,19 @@ class Podcast { toJSONForMetadataFile() { return { tags: [...this.tags], - metadata: this.metadata.toJSON() + title: this.metadata.title, + author: this.metadata.author, + description: this.metadata.description, + releaseDate: this.metadata.releaseDate, + genres: [...this.metadata.genres], + feedURL: this.metadata.feedUrl, + imageURL: this.metadata.imageUrl, + itunesPageURL: this.metadata.itunesPageUrl, + itunesId: this.metadata.itunesId, + itunesArtistId: this.metadata.itunesArtistId, + explicit: this.metadata.explicit, + language: this.metadata.language, + podcastType: this.metadata.type } } diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 5c0d9dad0..f31aaf6be 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -1,3 +1,4 @@ +const packageJson = require('../../../package.json') const { BookshelfView } = require('../../utils/constants') const Logger = require('../../Logger') @@ -50,7 +51,8 @@ class ServerSettings { this.logLevel = Logger.logLevel - this.version = null + this.version = packageJson.version + this.buildNumber = packageJson.buildNumber if (settings) { this.construct(settings) @@ -90,6 +92,7 @@ class ServerSettings { this.language = settings.language || 'en-us' this.logLevel = settings.logLevel || Logger.logLevel this.version = settings.version || null + this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 // Migrations if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was renamed to storeCoverWithItem in 2.0.0 @@ -106,9 +109,9 @@ class ServerSettings { this.metadataFileFormat = 'abs' } - // Validation - if (!['abs', 'json'].includes(this.metadataFileFormat)) { - Logger.error(`[ServerSettings] construct: Invalid metadataFileFormat ${this.metadataFileFormat}`) + // As of v2.4.5 only json is supported + if (this.metadataFileFormat !== 'json') { + Logger.warn(`[ServerSettings] Invalid metadataFileFormat ${this.metadataFileFormat} (as of v2.4.5 only json is supported)`) this.metadataFileFormat = 'json' } @@ -146,7 +149,8 @@ class ServerSettings { timeFormat: this.timeFormat, language: this.language, logLevel: this.logLevel, - version: this.version + version: this.version, + buildNumber: this.buildNumber } } diff --git a/server/scanner/AbsMetadataFileScanner.js b/server/scanner/AbsMetadataFileScanner.js index 037726f6f..1f9d28230 100644 --- a/server/scanner/AbsMetadataFileScanner.js +++ b/server/scanner/AbsMetadataFileScanner.js @@ -8,7 +8,7 @@ class AbsMetadataFileScanner { constructor() { } /** - * Check for metadata.json or metadata.abs file and set book metadata + * Check for metadata.json file and set book metadata * * @param {import('./LibraryScan')} libraryScan * @param {import('./LibraryItemScanData')} libraryItemData @@ -16,54 +16,36 @@ class AbsMetadataFileScanner { * @param {string} [existingLibraryItemId] */ async scanBookMetadataFile(libraryScan, libraryItemData, bookMetadata, existingLibraryItemId = null) { - const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile || libraryItemData.metadataAbsLibraryFile + const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile let metadataText = metadataLibraryFile ? await readTextFile(metadataLibraryFile.metadata.path) : null let metadataFilePath = metadataLibraryFile?.metadata.path - let metadataFileFormat = libraryItemData.metadataJsonLibraryFile ? 'json' : 'abs' // When metadata file is not stored with library item then check in the /metadata/items folder for it if (!metadataText && existingLibraryItemId) { let metadataPath = Path.join(global.MetadataPath, 'items', existingLibraryItemId) - let altFormat = global.ServerSettings.metadataFileFormat === 'json' ? 'abs' : 'json' - // First check the metadata format set in server settings, fallback to the alternate - metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) - metadataFileFormat = global.ServerSettings.metadataFileFormat + metadataFilePath = Path.join(metadataPath, 'metadata.json') if (await fsExtra.pathExists(metadataFilePath)) { metadataText = await readTextFile(metadataFilePath) - } else if (await fsExtra.pathExists(Path.join(metadataPath, `metadata.${altFormat}`))) { - metadataFilePath = Path.join(metadataPath, `metadata.${altFormat}`) - metadataFileFormat = altFormat - metadataText = await readTextFile(metadataFilePath) } } if (metadataText) { - libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}" - preferring`) - let abMetadata = null - if (metadataFileFormat === 'json') { - abMetadata = abmetadataGenerator.parseJson(metadataText) - } else { - abMetadata = abmetadataGenerator.parse(metadataText, 'book') - } + libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}"`) + const abMetadata = abmetadataGenerator.parseJson(metadataText) || {} + for (const key in abMetadata) { + // TODO: When to override with null or empty arrays? + if (abMetadata[key] === undefined || abMetadata[key] === null) continue + if (key === 'tags' && !abMetadata.tags?.length) continue + if (key === 'chapters' && !abMetadata.chapters?.length) continue - if (abMetadata) { - if (abMetadata.tags?.length) { - bookMetadata.tags = abMetadata.tags - } - if (abMetadata.chapters?.length) { - bookMetadata.chapters = abMetadata.chapters - } - for (const key in abMetadata.metadata) { - if (abMetadata.metadata[key] === undefined || abMetadata.metadata[key] === null) continue - bookMetadata[key] = abMetadata.metadata[key] - } + bookMetadata[key] = abMetadata[key] } } } /** - * Check for metadata.json or metadata.abs file and set podcast metadata + * Check for metadata.json file and set podcast metadata * * @param {import('./LibraryScan')} libraryScan * @param {import('./LibraryItemScanData')} libraryItemData @@ -71,53 +53,28 @@ class AbsMetadataFileScanner { * @param {string} [existingLibraryItemId] */ async scanPodcastMetadataFile(libraryScan, libraryItemData, podcastMetadata, existingLibraryItemId = null) { - const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile || libraryItemData.metadataAbsLibraryFile + const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile let metadataText = metadataLibraryFile ? await readTextFile(metadataLibraryFile.metadata.path) : null let metadataFilePath = metadataLibraryFile?.metadata.path - let metadataFileFormat = libraryItemData.metadataJsonLibraryFile ? 'json' : 'abs' // When metadata file is not stored with library item then check in the /metadata/items folder for it if (!metadataText && existingLibraryItemId) { let metadataPath = Path.join(global.MetadataPath, 'items', existingLibraryItemId) - let altFormat = global.ServerSettings.metadataFileFormat === 'json' ? 'abs' : 'json' - // First check the metadata format set in server settings, fallback to the alternate - metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) - metadataFileFormat = global.ServerSettings.metadataFileFormat + metadataFilePath = Path.join(metadataPath, 'metadata.json') if (await fsExtra.pathExists(metadataFilePath)) { metadataText = await readTextFile(metadataFilePath) - } else if (await fsExtra.pathExists(Path.join(metadataPath, `metadata.${altFormat}`))) { - metadataFilePath = Path.join(metadataPath, `metadata.${altFormat}`) - metadataFileFormat = altFormat - metadataText = await readTextFile(metadataFilePath) } } if (metadataText) { - libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}" - preferring`) - let abMetadata = null - if (metadataFileFormat === 'json') { - abMetadata = abmetadataGenerator.parseJson(metadataText) - } else { - abMetadata = abmetadataGenerator.parse(metadataText, 'podcast') - } + libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}"`) + const abMetadata = abmetadataGenerator.parseJson(metadataText) || {} + for (const key in abMetadata) { + if (abMetadata[key] === undefined || abMetadata[key] === null) continue + if (key === 'tags' && !abMetadata.tags?.length) continue - if (abMetadata) { - if (abMetadata.tags?.length) { - podcastMetadata.tags = abMetadata.tags - } - for (const key in abMetadata.metadata) { - if (abMetadata.metadata[key] === undefined) continue - - // TODO: New podcast model changed some keys, need to update the abmetadataGenerator - let newModelKey = key - if (key === 'feedUrl') newModelKey = 'feedURL' - else if (key === 'imageUrl') newModelKey = 'imageURL' - else if (key === 'itunesPageUrl') newModelKey = 'itunesPageURL' - else if (key === 'type') newModelKey = 'podcastType' - - podcastMetadata[newModelKey] = abMetadata.metadata[key] - } + podcastMetadata[key] = abMetadata[key] } } } diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index f752417c1..282155f26 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -678,10 +678,10 @@ class BookScanner { } /** - * Metadata from metadata.json or metadata.abs + * Metadata from metadata.json */ async absMetadata() { - // If metadata.json or metadata.abs use this for metadata + // If metadata.json use this for metadata await AbsMetadataFileScanner.scanBookMetadataFile(this.libraryScan, this.libraryItemData, this.bookMetadata, this.existingLibraryItemId) } } @@ -703,121 +703,66 @@ class BookScanner { await fsExtra.ensureDir(metadataPath) } - const metadataFileFormat = global.ServerSettings.metadataFileFormat - const metadataFilePath = Path.join(metadataPath, `metadata.${metadataFileFormat}`) - if (metadataFileFormat === 'json') { - // Remove metadata.abs if it exists - if (await fsExtra.pathExists(Path.join(metadataPath, `metadata.abs`))) { - libraryScan.addLog(LogLevel.DEBUG, `Removing metadata.abs for item "${libraryItem.media.title}"`) - await fsExtra.remove(Path.join(metadataPath, `metadata.abs`)) - libraryItem.libraryFiles = libraryItem.libraryFiles.filter(lf => lf.metadata.path !== filePathToPOSIX(Path.join(metadataPath, `metadata.abs`))) - } + const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) - // TODO: Update to not use `metadata` so it fits the updated model - const jsonObject = { - tags: libraryItem.media.tags || [], - chapters: libraryItem.media.chapters?.map(c => ({ ...c })) || [], - metadata: { - title: libraryItem.media.title, - subtitle: libraryItem.media.subtitle, - authors: libraryItem.media.authors.map(a => a.name), - narrators: libraryItem.media.narrators, - series: libraryItem.media.series.map(se => { - const sequence = se.bookSeries?.sequence || '' - if (!sequence) return se.name - return `${se.name} #${sequence}` - }), - genres: libraryItem.media.genres || [], - publishedYear: libraryItem.media.publishedYear, - publishedDate: libraryItem.media.publishedDate, - publisher: libraryItem.media.publisher, - description: libraryItem.media.description, - isbn: libraryItem.media.isbn, - asin: libraryItem.media.asin, - language: libraryItem.media.language, - explicit: !!libraryItem.media.explicit, - abridged: !!libraryItem.media.abridged - } - } - return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => { - // Add metadata.json to libraryFiles array if it is new - let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) - if (storeMetadataWithItem) { - if (!metadataLibraryFile) { - const newLibraryFile = new LibraryFile() - await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) - metadataLibraryFile = newLibraryFile.toJSON() - libraryItem.libraryFiles.push(metadataLibraryFile) - } else { - const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) - if (fileTimestamps) { - metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs - metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs - metadataLibraryFile.metadata.size = fileTimestamps.size - metadataLibraryFile.ino = fileTimestamps.ino - } - } - const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path) - if (libraryItemDirTimestamps) { - libraryItem.mtime = libraryItemDirTimestamps.mtimeMs - libraryItem.ctime = libraryItemDirTimestamps.ctimeMs - let size = 0 - libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) - libraryItem.size = size - } - } - - libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`) - - return metadataLibraryFile - }).catch((error) => { - libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error) - return null - }) - } else { - // Remove metadata.json if it exists - if (await fsExtra.pathExists(Path.join(metadataPath, `metadata.json`))) { - libraryScan.addLog(LogLevel.DEBUG, `Removing metadata.json for item "${libraryItem.media.title}"`) - await fsExtra.remove(Path.join(metadataPath, `metadata.json`)) - libraryItem.libraryFiles = libraryItem.libraryFiles.filter(lf => lf.metadata.path !== filePathToPOSIX(Path.join(metadataPath, `metadata.json`))) - } - - return abmetadataGenerator.generateFromNewModel(libraryItem, metadataFilePath).then(async (success) => { - if (!success) { - libraryScan.addLog(LogLevel.ERROR, `Failed saving abmetadata to "${metadataFilePath}"`) - return null - } - // Add metadata.abs to libraryFiles array if it is new - let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) - if (storeMetadataWithItem) { - if (!metadataLibraryFile) { - const newLibraryFile = new LibraryFile() - await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.abs`) - metadataLibraryFile = newLibraryFile.toJSON() - libraryItem.libraryFiles.push(metadataLibraryFile) - } else { - const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) - if (fileTimestamps) { - metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs - metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs - metadataLibraryFile.metadata.size = fileTimestamps.size - metadataLibraryFile.ino = fileTimestamps.ino - } - } - const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path) - if (libraryItemDirTimestamps) { - libraryItem.mtime = libraryItemDirTimestamps.mtimeMs - libraryItem.ctime = libraryItemDirTimestamps.ctimeMs - let size = 0 - libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) - libraryItem.size = size - } - } - - libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`) - return metadataLibraryFile - }) + const jsonObject = { + tags: libraryItem.media.tags || [], + chapters: libraryItem.media.chapters?.map(c => ({ ...c })) || [], + title: libraryItem.media.title, + subtitle: libraryItem.media.subtitle, + authors: libraryItem.media.authors.map(a => a.name), + narrators: libraryItem.media.narrators, + series: libraryItem.media.series.map(se => { + const sequence = se.bookSeries?.sequence || '' + if (!sequence) return se.name + return `${se.name} #${sequence}` + }), + genres: libraryItem.media.genres || [], + publishedYear: libraryItem.media.publishedYear, + publishedDate: libraryItem.media.publishedDate, + publisher: libraryItem.media.publisher, + description: libraryItem.media.description, + isbn: libraryItem.media.isbn, + asin: libraryItem.media.asin, + language: libraryItem.media.language, + explicit: !!libraryItem.media.explicit, + abridged: !!libraryItem.media.abridged } + return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => { + // Add metadata.json to libraryFiles array if it is new + let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) + if (storeMetadataWithItem) { + if (!metadataLibraryFile) { + const newLibraryFile = new LibraryFile() + await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) + metadataLibraryFile = newLibraryFile.toJSON() + libraryItem.libraryFiles.push(metadataLibraryFile) + } else { + const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) + if (fileTimestamps) { + metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs + metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs + metadataLibraryFile.metadata.size = fileTimestamps.size + metadataLibraryFile.ino = fileTimestamps.ino + } + } + const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path) + if (libraryItemDirTimestamps) { + libraryItem.mtime = libraryItemDirTimestamps.mtimeMs + libraryItem.ctime = libraryItemDirTimestamps.ctimeMs + let size = 0 + libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) + libraryItem.size = size + } + } + + libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`) + + return metadataLibraryFile + }).catch((error) => { + libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error) + return null + }) } /** diff --git a/server/scanner/PodcastScanner.js b/server/scanner/PodcastScanner.js index 53d4ad1f7..b56c4db60 100644 --- a/server/scanner/PodcastScanner.js +++ b/server/scanner/PodcastScanner.js @@ -342,7 +342,7 @@ class PodcastScanner { AudioFileScanner.setPodcastMetadataFromAudioMetaTags(podcastEpisodes[0].audioFile, podcastMetadata, libraryScan) } - // Use metadata.json or metadata.abs file + // Use metadata.json file await AbsMetadataFileScanner.scanPodcastMetadataFile(libraryScan, libraryItemData, podcastMetadata, existingLibraryItemId) podcastMetadata.titleIgnorePrefix = getTitleIgnorePrefix(podcastMetadata.title) @@ -367,115 +367,60 @@ class PodcastScanner { await fsExtra.ensureDir(metadataPath) } - const metadataFileFormat = global.ServerSettings.metadataFileFormat - const metadataFilePath = Path.join(metadataPath, `metadata.${metadataFileFormat}`) - if (metadataFileFormat === 'json') { - // Remove metadata.abs if it exists - if (await fsExtra.pathExists(Path.join(metadataPath, `metadata.abs`))) { - libraryScan.addLog(LogLevel.DEBUG, `Removing metadata.abs for item "${libraryItem.media.title}"`) - await fsExtra.remove(Path.join(metadataPath, `metadata.abs`)) - libraryItem.libraryFiles = libraryItem.libraryFiles.filter(lf => lf.metadata.path !== filePathToPOSIX(Path.join(metadataPath, `metadata.abs`))) - } + const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) - // TODO: Update to not use `metadata` so it fits the updated model - const jsonObject = { - tags: libraryItem.media.tags || [], - metadata: { - title: libraryItem.media.title, - author: libraryItem.media.author, - description: libraryItem.media.description, - releaseDate: libraryItem.media.releaseDate, - genres: libraryItem.media.genres || [], - feedUrl: libraryItem.media.feedURL, - imageUrl: libraryItem.media.imageURL, - itunesPageUrl: libraryItem.media.itunesPageURL, - itunesId: libraryItem.media.itunesId, - itunesArtistId: libraryItem.media.itunesArtistId, - asin: libraryItem.media.asin, - language: libraryItem.media.language, - explicit: !!libraryItem.media.explicit, - type: libraryItem.media.podcastType - } - } - return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => { - // Add metadata.json to libraryFiles array if it is new - let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) - if (storeMetadataWithItem) { - if (!metadataLibraryFile) { - const newLibraryFile = new LibraryFile() - await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) - metadataLibraryFile = newLibraryFile.toJSON() - libraryItem.libraryFiles.push(metadataLibraryFile) - } else { - const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) - if (fileTimestamps) { - metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs - metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs - metadataLibraryFile.metadata.size = fileTimestamps.size - metadataLibraryFile.ino = fileTimestamps.ino - } - } - const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path) - if (libraryItemDirTimestamps) { - libraryItem.mtime = libraryItemDirTimestamps.mtimeMs - libraryItem.ctime = libraryItemDirTimestamps.ctimeMs - let size = 0 - libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) - libraryItem.size = size - } - } - - libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`) - - return metadataLibraryFile - }).catch((error) => { - libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error) - return null - }) - } else { - // Remove metadata.json if it exists - if (await fsExtra.pathExists(Path.join(metadataPath, `metadata.json`))) { - libraryScan.addLog(LogLevel.DEBUG, `Removing metadata.json for item "${libraryItem.media.title}"`) - await fsExtra.remove(Path.join(metadataPath, `metadata.json`)) - libraryItem.libraryFiles = libraryItem.libraryFiles.filter(lf => lf.metadata.path !== filePathToPOSIX(Path.join(metadataPath, `metadata.json`))) - } - - return abmetadataGenerator.generateFromNewModel(libraryItem, metadataFilePath).then(async (success) => { - if (!success) { - libraryScan.addLog(LogLevel.ERROR, `Failed saving abmetadata to "${metadataFilePath}"`) - return null - } - // Add metadata.abs to libraryFiles array if it is new - let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) - if (storeMetadataWithItem) { - if (!metadataLibraryFile) { - const newLibraryFile = new LibraryFile() - await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.abs`) - metadataLibraryFile = newLibraryFile.toJSON() - libraryItem.libraryFiles.push(metadataLibraryFile) - } else { - const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) - if (fileTimestamps) { - metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs - metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs - metadataLibraryFile.metadata.size = fileTimestamps.size - metadataLibraryFile.ino = fileTimestamps.ino - } - } - const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path) - if (libraryItemDirTimestamps) { - libraryItem.mtime = libraryItemDirTimestamps.mtimeMs - libraryItem.ctime = libraryItemDirTimestamps.ctimeMs - let size = 0 - libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) - libraryItem.size = size - } - } - - libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`) - return metadataLibraryFile - }) + const jsonObject = { + tags: libraryItem.media.tags || [], + title: libraryItem.media.title, + author: libraryItem.media.author, + description: libraryItem.media.description, + releaseDate: libraryItem.media.releaseDate, + genres: libraryItem.media.genres || [], + feedURL: libraryItem.media.feedURL, + imageURL: libraryItem.media.imageURL, + itunesPageURL: libraryItem.media.itunesPageURL, + itunesId: libraryItem.media.itunesId, + itunesArtistId: libraryItem.media.itunesArtistId, + asin: libraryItem.media.asin, + language: libraryItem.media.language, + explicit: !!libraryItem.media.explicit, + podcastType: libraryItem.media.podcastType } + return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => { + // Add metadata.json to libraryFiles array if it is new + let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) + if (storeMetadataWithItem) { + if (!metadataLibraryFile) { + const newLibraryFile = new LibraryFile() + await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) + metadataLibraryFile = newLibraryFile.toJSON() + libraryItem.libraryFiles.push(metadataLibraryFile) + } else { + const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) + if (fileTimestamps) { + metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs + metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs + metadataLibraryFile.metadata.size = fileTimestamps.size + metadataLibraryFile.ino = fileTimestamps.ino + } + } + const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path) + if (libraryItemDirTimestamps) { + libraryItem.mtime = libraryItemDirTimestamps.mtimeMs + libraryItem.ctime = libraryItemDirTimestamps.ctimeMs + let size = 0 + libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) + libraryItem.size = size + } + } + + libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`) + + return metadataLibraryFile + }).catch((error) => { + libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error) + return null + }) } } module.exports = new PodcastScanner() \ No newline at end of file diff --git a/server/utils/generators/abmetadataGenerator.js b/server/utils/generators/abmetadataGenerator.js index ff82ac33f..e0b78d2e0 100644 --- a/server/utils/generators/abmetadataGenerator.js +++ b/server/utils/generators/abmetadataGenerator.js @@ -1,461 +1,26 @@ -const fs = require('../../libs/fsExtra') -const package = require('../../../package.json') const Logger = require('../../Logger') -const { getId } = require('../index') -const areEquivalent = require('../areEquivalent') - - -const CurrentAbMetadataVersion = 2 -// abmetadata v1 key map -// const bookKeyMap = { -// title: 'title', -// subtitle: 'subtitle', -// author: 'authorFL', -// narrator: 'narratorFL', -// publishedYear: 'publishedYear', -// publisher: 'publisher', -// description: 'description', -// isbn: 'isbn', -// asin: 'asin', -// language: 'language', -// genres: 'genresCommaSeparated' -// } - -const commaSeparatedToArray = (v) => { - if (!v) return [] - return [...new Set(v.split(',').map(_v => _v.trim()).filter(_v => _v))] -} - -const podcastMetadataMapper = { - title: { - to: (m) => m.title || '', - from: (v) => v || '' - }, - author: { - to: (m) => m.author || '', - from: (v) => v || null - }, - language: { - to: (m) => m.language || '', - from: (v) => v || null - }, - genres: { - to: (m) => m.genres?.join(', ') || '', - from: (v) => commaSeparatedToArray(v) - }, - feedUrl: { - to: (m) => m.feedUrl || '', - from: (v) => v || null - }, - itunesId: { - to: (m) => m.itunesId || '', - from: (v) => v || null - }, - explicit: { - to: (m) => m.explicit ? 'Y' : 'N', - from: (v) => v && v.toLowerCase() == 'y' - } -} - -const bookMetadataMapper = { - title: { - to: (m) => m.title || '', - from: (v) => v || '' - }, - subtitle: { - to: (m) => m.subtitle || '', - from: (v) => v || null - }, - authors: { - to: (m) => { - if (m.authorName !== undefined) return m.authorName - if (!m.authors?.length) return '' - return m.authors.map(au => au.name).join(', ') - }, - from: (v) => commaSeparatedToArray(v) - }, - narrators: { - to: (m) => m.narrators?.join(', ') || '', - from: (v) => commaSeparatedToArray(v) - }, - publishedYear: { - to: (m) => m.publishedYear || '', - from: (v) => v || null - }, - publisher: { - to: (m) => m.publisher || '', - from: (v) => v || null - }, - isbn: { - to: (m) => m.isbn || '', - from: (v) => v || null - }, - asin: { - to: (m) => m.asin || '', - from: (v) => v || null - }, - language: { - to: (m) => m.language || '', - from: (v) => v || null - }, - genres: { - to: (m) => m.genres?.join(', ') || '', - from: (v) => commaSeparatedToArray(v) - }, - series: { - to: (m) => { - if (m.seriesName !== undefined) return m.seriesName - if (!m.series?.length) return '' - return m.series.map((se) => { - const sequence = se.bookSeries?.sequence || '' - if (!sequence) return se.name - return `${se.name} #${sequence}` - }).join(', ') - }, - from: (v) => { - return commaSeparatedToArray(v).map(series => { // Return array of { name, sequence } - let sequence = null - let name = series - // Series sequence match any characters after " #" other than whitespace and another # - // e.g. "Name #1a" is valid. "Name #1#a" or "Name #1 a" is not valid. - const matchResults = series.match(/ #([^#\s]+)$/) // Pull out sequence # - if (matchResults && matchResults.length && matchResults.length > 1) { - sequence = matchResults[1] // Group 1 - name = series.replace(matchResults[0], '') - } - return { - name, - sequence - } - }) - } - }, - explicit: { - to: (m) => m.explicit ? 'Y' : 'N', - from: (v) => v && v.toLowerCase() == 'y' - }, - abridged: { - to: (m) => m.abridged ? 'Y' : 'N', - from: (v) => v && v.toLowerCase() == 'y' - } -} - -const metadataMappers = { - book: bookMetadataMapper, - podcast: podcastMetadataMapper -} - -function generate(libraryItem, outputPath) { - let fileString = `;ABMETADATA${CurrentAbMetadataVersion}\n` - fileString += `#audiobookshelf v${package.version}\n\n` - - const mediaType = libraryItem.mediaType - - fileString += `media=${mediaType}\n` - fileString += `tags=${JSON.stringify(libraryItem.media.tags)}\n` - - const metadataMapper = metadataMappers[mediaType] - var mediaMetadata = libraryItem.media.metadata - for (const key in metadataMapper) { - fileString += `${key}=${metadataMapper[key].to(mediaMetadata)}\n` - } - - // Description block - if (mediaMetadata.description) { - fileString += '\n[DESCRIPTION]\n' - fileString += mediaMetadata.description + '\n' - } - - // Book chapters - if (libraryItem.mediaType == 'book' && libraryItem.media.chapters.length) { - fileString += '\n' - libraryItem.media.chapters.forEach((chapter) => { - fileString += `[CHAPTER]\n` - fileString += `start=${chapter.start}\n` - fileString += `end=${chapter.end}\n` - fileString += `title=${chapter.title}\n` - }) - } - return fs.writeFile(outputPath, fileString).then(() => true).catch((error) => { - Logger.error(`[absMetaFileGenerator] Failed to save abs file`, error) - return false - }) -} -module.exports.generate = generate - -function generateFromNewModel(libraryItem, outputPath) { - let fileString = `;ABMETADATA${CurrentAbMetadataVersion}\n` - fileString += `#audiobookshelf v${package.version}\n\n` - - const mediaType = libraryItem.mediaType - - fileString += `media=${mediaType}\n` - fileString += `tags=${JSON.stringify(libraryItem.media.tags || '')}\n` - - const metadataMapper = metadataMappers[mediaType] - for (const key in metadataMapper) { - fileString += `${key}=${metadataMapper[key].to(libraryItem.media)}\n` - } - - // Description block - if (libraryItem.media.description) { - fileString += '\n[DESCRIPTION]\n' - fileString += libraryItem.media.description + '\n' - } - - // Book chapters - if (mediaType == 'book' && libraryItem.media.chapters?.length) { - fileString += '\n' - libraryItem.media.chapters.forEach((chapter) => { - fileString += `[CHAPTER]\n` - fileString += `start=${chapter.start}\n` - fileString += `end=${chapter.end}\n` - fileString += `title=${chapter.title}\n` - }) - } - return fs.writeFile(outputPath, fileString).then(() => true).catch((error) => { - Logger.error(`[absMetaFileGenerator] Failed to save abs file`, error) - return false - }) -} -module.exports.generateFromNewModel = generateFromNewModel - -function parseSections(lines) { - if (!lines || !lines.length || !lines[0].startsWith('[')) { // First line must be section start - return [] - } - - var sections = [] - var currentSection = [] - lines.forEach(line => { - if (!line || !line.trim()) return - - if (line.startsWith('[') && currentSection.length) { // current section ended - sections.push(currentSection) - currentSection = [] - } - - currentSection.push(line) - }) - if (currentSection.length) sections.push(currentSection) - return sections -} - -// lines inside chapter section -function parseChapterLines(lines) { - var chapter = { - start: null, - end: null, - title: null - } - - lines.forEach((line) => { - var keyValue = line.split('=') - if (keyValue.length > 1) { - var key = keyValue[0].trim() - var value = keyValue[1].trim() - - if (key === 'start' || key === 'end') { - if (!isNaN(value)) { - chapter[key] = Number(value) - } else { - Logger.warn(`[abmetadataGenerator] Invalid chapter value for ${key}: ${value}`) - } - } else if (key === 'title') { - chapter[key] = value - } - } - }) - - if (chapter.start === null || chapter.end === null || chapter.end < chapter.start) { - Logger.warn(`[abmetadataGenerator] Invalid chapter`) - return null - } - return chapter -} - -function parseTags(value) { - if (!value) return null - try { - const parsedTags = [] - JSON.parse(value).forEach((loadedTag) => { - if (loadedTag.trim()) parsedTags.push(loadedTag) // Only push tags that are non-empty - }) - return parsedTags - } catch (err) { - Logger.error(`[abmetadataGenerator] Error parsing TAGS "${value}":`, err.message) - return null - } -} - -function parseAbMetadataText(text, mediaType) { - if (!text) return null - let lines = text.split(/\r?\n/) - - // Check first line and get abmetadata version number - const firstLine = lines.shift().toLowerCase() - if (!firstLine.startsWith(';abmetadata')) { - Logger.error(`Invalid abmetadata file first line is not ;abmetadata "${firstLine}"`) - return null - } - const abmetadataVersion = Number(firstLine.replace(';abmetadata', '').trim()) - if (isNaN(abmetadataVersion) || abmetadataVersion != CurrentAbMetadataVersion) { - Logger.warn(`Invalid abmetadata version ${abmetadataVersion} - must use version ${CurrentAbMetadataVersion}`) - return null - } - - // Remove comments and empty lines - const ignoreFirstChars = [' ', '#', ';'] // Ignore any line starting with the following - lines = lines.filter(line => !!line.trim() && !ignoreFirstChars.includes(line[0])) - - // Get lines that map to book details (all lines before the first chapter or description section) - const firstSectionLine = lines.findIndex(l => l.startsWith('[')) - const detailLines = firstSectionLine > 0 ? lines.slice(0, firstSectionLine) : lines - const remainingLines = firstSectionLine > 0 ? lines.slice(firstSectionLine) : [] - - if (!detailLines.length) { - Logger.error(`Invalid abmetadata file no detail lines`) - return null - } - - // Check the media type saved for this abmetadata file show warning if not matching expected - if (detailLines[0].toLowerCase().startsWith('media=')) { - const mediaLine = detailLines.shift() // Remove media line - const abMediaType = mediaLine.toLowerCase().split('=')[1].trim() - if (abMediaType != mediaType) { - Logger.warn(`Invalid media type in abmetadata file ${abMediaType} expecting ${mediaType}`) - } - } else { - Logger.warn(`No media type found in abmetadata file - expecting ${mediaType}`) - } - - const metadataMapper = metadataMappers[mediaType] - // Put valid book detail values into map - const mediaDetails = { - metadata: {}, - chapters: [], - tags: null // When tags are null it will not be used - } - - for (let i = 0; i < detailLines.length; i++) { - const line = detailLines[i] - const keyValue = line.split('=') - if (keyValue.length < 2) { - Logger.warn('abmetadata invalid line has no =', line) - } else if (keyValue[0].trim() === 'tags') { // Parse tags - const value = keyValue.slice(1).join('=').trim() // Everything after "tags=" - mediaDetails.tags = parseTags(value) - } else if (!metadataMapper[keyValue[0].trim()]) { // Ensure valid media metadata key - Logger.warn(`abmetadata key "${keyValue[0].trim()}" is not a valid ${mediaType} metadata key`) - } else { - const key = keyValue.shift().trim() - const value = keyValue.join('=').trim() - mediaDetails.metadata[key] = metadataMapper[key].from(value) - } - } - - // Parse sections for description and chapters - const sections = parseSections(remainingLines) - sections.forEach((section) => { - const sectionHeader = section.shift() - if (sectionHeader.toLowerCase().startsWith('[description]')) { - mediaDetails.metadata.description = section.join('\n') - } else if (sectionHeader.toLowerCase().startsWith('[chapter]')) { - const chapter = parseChapterLines(section) - if (chapter) { - mediaDetails.chapters.push(chapter) - } - } - }) - - mediaDetails.chapters.sort((a, b) => a.start - b.start) - - if (mediaDetails.chapters.length) { - mediaDetails.chapters = cleanChaptersArray(mediaDetails.chapters, mediaDetails.metadata.title) || [] - } - - return mediaDetails -} -module.exports.parse = parseAbMetadataText - -function checkUpdatedBookAuthors(abmetadataAuthors, authors) { - const finalAuthors = [] - let hasUpdates = false - - abmetadataAuthors.forEach((authorName) => { - const findAuthor = authors.find(au => au.name.toLowerCase() == authorName.toLowerCase()) - if (!findAuthor) { - hasUpdates = true - finalAuthors.push({ - id: getId('new'), // New author gets created in Scanner.js after library scan - name: authorName - }) - } else { - finalAuthors.push(findAuthor) - } - }) - - var authorsRemoved = authors.filter(au => !abmetadataAuthors.some(auname => auname.toLowerCase() == au.name.toLowerCase())) - if (authorsRemoved.length) { - hasUpdates = true - } - - return { - authors: finalAuthors, - hasUpdates - } -} - -function checkUpdatedBookSeries(abmetadataSeries, series) { - var finalSeries = [] - var hasUpdates = false - - abmetadataSeries.forEach((seriesObj) => { - var findSeries = series.find(se => se.name.toLowerCase() == seriesObj.name.toLowerCase()) - if (!findSeries) { - hasUpdates = true - finalSeries.push({ - id: getId('new'), // New series gets created in Scanner.js after library scan - name: seriesObj.name, - sequence: seriesObj.sequence - }) - } else if (findSeries.sequence != seriesObj.sequence) { // Sequence was updated - hasUpdates = true - finalSeries.push({ - id: findSeries.id, - name: findSeries.name, - sequence: seriesObj.sequence - }) - } else { - finalSeries.push(findSeries) - } - }) - - var seriesRemoved = series.filter(se => !abmetadataSeries.some(_se => _se.name.toLowerCase() == se.name.toLowerCase())) - if (seriesRemoved.length) { - hasUpdates = true - } - - return { - series: finalSeries, - hasUpdates - } -} - -function checkArraysChanged(abmetadataArray, mediaArray) { - if (!Array.isArray(abmetadataArray)) return false - if (!Array.isArray(mediaArray)) return true - return abmetadataArray.join(',') != mediaArray.join(',') -} function parseJsonMetadataText(text) { try { const abmetadataData = JSON.parse(text) - if (!abmetadataData.metadata) abmetadataData.metadata = {} - if (abmetadataData.metadata.series?.length) { - abmetadataData.metadata.series = [...new Set(abmetadataData.metadata.series.map(t => t?.trim()).filter(t => t))] - abmetadataData.metadata.series = abmetadataData.metadata.series.map(series => { + // Old metadata.json used nested "metadata" + if (abmetadataData.metadata) { + for (const key in abmetadataData.metadata) { + if (abmetadataData.metadata[key] === undefined) continue + let newModelKey = key + if (key === 'feedUrl') newModelKey = 'feedURL' + else if (key === 'imageUrl') newModelKey = 'imageURL' + else if (key === 'itunesPageUrl') newModelKey = 'itunesPageURL' + else if (key === 'type') newModelKey = 'podcastType' + abmetadataData[newModelKey] = abmetadataData.metadata[key] + } + } + delete abmetadataData.metadata + + if (abmetadataData.series?.length) { + abmetadataData.series = [...new Set(abmetadataData.series.map(t => t?.trim()).filter(t => t))] + abmetadataData.series = abmetadataData.series.map(series => { let sequence = null let name = series // Series sequence match any characters after " #" other than whitespace and another # @@ -476,17 +41,17 @@ function parseJsonMetadataText(text) { abmetadataData.tags = [...new Set(abmetadataData.tags.map(t => t?.trim()).filter(t => t))] } if (abmetadataData.chapters?.length) { - abmetadataData.chapters = cleanChaptersArray(abmetadataData.chapters, abmetadataData.metadata.title) + abmetadataData.chapters = cleanChaptersArray(abmetadataData.chapters, abmetadataData.title) } // clean remove dupes - if (abmetadataData.metadata.authors?.length) { - abmetadataData.metadata.authors = [...new Set(abmetadataData.metadata.authors.map(t => t?.trim()).filter(t => t))] + if (abmetadataData.authors?.length) { + abmetadataData.authors = [...new Set(abmetadataData.authors.map(t => t?.trim()).filter(t => t))] } - if (abmetadataData.metadata.narrators?.length) { - abmetadataData.metadata.narrators = [...new Set(abmetadataData.metadata.narrators.map(t => t?.trim()).filter(t => t))] + if (abmetadataData.narrators?.length) { + abmetadataData.narrators = [...new Set(abmetadataData.narrators.map(t => t?.trim()).filter(t => t))] } - if (abmetadataData.metadata.genres?.length) { - abmetadataData.metadata.genres = [...new Set(abmetadataData.metadata.genres.map(t => t?.trim()).filter(t => t))] + if (abmetadataData.genres?.length) { + abmetadataData.genres = [...new Set(abmetadataData.genres.map(t => t?.trim()).filter(t => t))] } return abmetadataData } catch (error) { @@ -522,73 +87,3 @@ function cleanChaptersArray(chaptersArray, mediaTitle) { } return chapters } - -// Input text from abmetadata file and return object of media changes -// only returns object of changes. empty object means no changes -function parseAndCheckForUpdates(text, media, mediaType, isJSON) { - if (!text || !media || !media.metadata || !mediaType) { - Logger.error(`Invalid inputs to parseAndCheckForUpdates`) - return null - } - - const mediaMetadata = media.metadata - const metadataUpdatePayload = {} // Only updated key/values - - let abmetadataData = null - - if (isJSON) { - abmetadataData = parseJsonMetadataText(text) - } else { - abmetadataData = parseAbMetadataText(text, mediaType) - } - - if (!abmetadataData || !abmetadataData.metadata) { - Logger.error(`[abmetadataGenerator] Invalid metadata file`) - return null - } - - const abMetadata = abmetadataData.metadata // Metadata from abmetadata file - for (const key in abMetadata) { - if (mediaMetadata[key] !== undefined) { - if (key === 'authors') { - const authorUpdatePayload = checkUpdatedBookAuthors(abMetadata[key], mediaMetadata[key]) - if (authorUpdatePayload.hasUpdates) metadataUpdatePayload.authors = authorUpdatePayload.authors - } else if (key === 'series') { - const seriesUpdatePayload = checkUpdatedBookSeries(abMetadata[key], mediaMetadata[key]) - if (seriesUpdatePayload.hasUpdates) metadataUpdatePayload.series = seriesUpdatePayload.series - } else if (key === 'genres' || key === 'narrators') { // Compare array differences - if (checkArraysChanged(abMetadata[key], mediaMetadata[key])) { - metadataUpdatePayload[key] = abMetadata[key] - } - } else if (abMetadata[key] !== mediaMetadata[key]) { - metadataUpdatePayload[key] = abMetadata[key] - } - } else { - Logger.warn('[abmetadataGenerator] Invalid key', key) - } - } - - const updatePayload = {} // Only updated key/values - // Check update tags - if (abmetadataData.tags) { - if (checkArraysChanged(abmetadataData.tags, media.tags)) { - updatePayload.tags = abmetadataData.tags - } - } - - if (abmetadataData.chapters && mediaType === 'book') { - const abmetadataChaptersCleaned = cleanChaptersArray(abmetadataData.chapters) - if (abmetadataChaptersCleaned) { - if (!areEquivalent(abmetadataChaptersCleaned, media.chapters)) { - updatePayload.chapters = abmetadataChaptersCleaned - } - } - } - - if (Object.keys(metadataUpdatePayload).length) { - updatePayload.metadata = metadataUpdatePayload - } - - return updatePayload -} -module.exports.parseAndCheckForUpdates = parseAndCheckForUpdates diff --git a/server/utils/migrations/absMetadataMigration.js b/server/utils/migrations/absMetadataMigration.js new file mode 100644 index 000000000..0d9f909a9 --- /dev/null +++ b/server/utils/migrations/absMetadataMigration.js @@ -0,0 +1,93 @@ +const Path = require('path') +const Logger = require('../../Logger') +const fsExtra = require('../../libs/fsExtra') +const fileUtils = require('../fileUtils') +const LibraryFile = require('../../objects/files/LibraryFile') + +/** + * + * @param {import('../../models/LibraryItem')} libraryItem + * @returns {Promise<boolean>} false if failed + */ +async function writeMetadataFileForItem(libraryItem) { + const storeMetadataWithItem = global.ServerSettings.storeMetadataWithItem && !libraryItem.isFile + const metadataPath = storeMetadataWithItem ? libraryItem.path : Path.join(global.MetadataPath, 'items', libraryItem.id) + const metadataFilepath = fileUtils.filePathToPOSIX(Path.join(metadataPath, 'metadata.json')) + if ((await fsExtra.pathExists(metadataFilepath))) { + // Metadata file already exists do nothing + return null + } + Logger.info(`[absMetadataMigration] metadata file not found at "${metadataFilepath}" - creating`) + + if (!storeMetadataWithItem) { + // Ensure /metadata/items/<lid> dir + await fsExtra.ensureDir(metadataPath) + } + + const metadataJson = libraryItem.media.getAbsMetadataJson() + + // Save to file + const success = await fsExtra.writeFile(metadataFilepath, JSON.stringify(metadataJson, null, 2)).then(() => true).catch((error) => { + Logger.error(`[absMetadataMigration] failed to save metadata file at "${metadataFilepath}"`, error.message || error) + return false + }) + + if (!success) return false + if (!storeMetadataWithItem) return true // No need to do anything else + + // Safety check to make sure library file with the same path isnt already there + libraryItem.libraryFiles = libraryItem.libraryFiles.filter(lf => lf.metadata.path !== metadataFilepath) + + // Put new library file in library item + const newLibraryFile = new LibraryFile() + await newLibraryFile.setDataFromPath(metadataFilepath, 'metadata.json') + libraryItem.libraryFiles.push(newLibraryFile.toJSON()) + + // Update library item timestamps and total size + const libraryItemDirTimestamps = await fileUtils.getFileTimestampsWithIno(libraryItem.path) + if (libraryItemDirTimestamps) { + libraryItem.mtime = libraryItemDirTimestamps.mtimeMs + libraryItem.ctime = libraryItemDirTimestamps.ctimeMs + let size = 0 + libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) + libraryItem.size = size + } + + libraryItem.changed('libraryFiles', true) + return libraryItem.save().then(() => true).catch((error) => { + Logger.error(`[absMetadataMigration] failed to save libraryItem "${libraryItem.id}"`, error.message || error) + return false + }) +} + +/** + * + * @param {import('../../Database')} Database + * @param {number} [offset=0] + * @param {number} [totalCreated=0] + */ +async function runMigration(Database, offset = 0, totalCreated = 0) { + const libraryItems = await Database.libraryItemModel.getLibraryItemsIncrement(offset, 500, { isMissing: false }) + if (!libraryItems.length) return totalCreated + + let numCreated = 0 + for (const libraryItem of libraryItems) { + const success = await writeMetadataFileForItem(libraryItem) + if (success) numCreated++ + } + + if (libraryItems.length < 500) { + return totalCreated + numCreated + } + return runMigration(Database, offset + libraryItems.length, totalCreated + numCreated) +} + +/** + * + * @param {import('../../Database')} Database + */ +module.exports.migrate = async (Database) => { + Logger.info(`[absMetadataMigration] Starting metadata.json migration`) + const totalCreated = await runMigration(Database) + Logger.info(`[absMetadataMigration] Finished metadata.json migration (${totalCreated} files created)`) +} \ No newline at end of file From 5a70c0d7bee676102d2b17c296fcf328f7248249 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 22 Oct 2023 16:40:12 -0500 Subject: [PATCH 081/208] Fix:Authors page books hide radio button on hover --- client/components/cards/LazyBookCard.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index d3f956ec6..1b87df0f2 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -68,7 +68,8 @@ <span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span> </div> - <div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick"> + <!-- Radio button --> + <div v-if="!isAuthorBookshelfView" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick"> <span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span> </div> From c4c12836a4c512b82c560a3fdfe8fa427edf3a2b Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 22 Oct 2023 17:04:45 -0500 Subject: [PATCH 082/208] Fix:Version in bottom left of siderail overlapping buttons #2195 --- client/components/app/ConfigSideNav.vue | 6 +- client/components/app/SideRail.vue | 161 ++++++++++++---------- client/components/widgets/CloseButton.vue | 33 ----- client/layouts/default.vue | 2 - 4 files changed, 89 insertions(+), 113 deletions(-) delete mode 100644 client/components/widgets/CloseButton.vue diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index 50e440d7b..267aabaa8 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -14,10 +14,10 @@ </div> <div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }"> - <div class="flex justify-between"> - <p class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</p> + <div class="flex items-center justify-between"> + <button type="button" class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</button> - <p class="font-mono text-xs text-gray-300 italic">{{ Source }}</p> + <p class="text-xs text-gray-300 italic">{{ Source }}</p> </div> <a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a> </div> diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index 995f4c232..deb96a6cb 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -3,117 +3,119 @@ <!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar --> <div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" /> - <nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> - <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /> - </svg> + <div id="siderail-buttons-container" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden"> + <nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /> + </svg> - <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p> + <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p> - <div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - </nuxt-link> + <div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + </nuxt-link> - <nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> - <span class="material-icons text-2xl">format_list_bulleted</span> + <nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> + <span class="material-icons text-2xl">format_list_bulleted</span> - <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p> + <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p> - <div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - </nuxt-link> + <div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + </nuxt-link> - <nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> - <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /> - </svg> + <nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /> + </svg> - <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p> + <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p> - <div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - </nuxt-link> + <div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + </nuxt-link> - <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> - <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" /> - </svg> + <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" /> + </svg> - <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p> + <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p> - <div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - </nuxt-link> + <div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + </nuxt-link> - <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> - <span class="material-icons-outlined text-2xl">collections_bookmark</span> + <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> + <span class="material-icons-outlined text-2xl">collections_bookmark</span> - <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p> + <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p> - <div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - </nuxt-link> + <div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + </nuxt-link> - <nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> - <span class="material-icons text-2.5xl">queue_music</span> + <nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> + <span class="material-icons text-2.5xl">queue_music</span> - <p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p> + <p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p> - <div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - </nuxt-link> + <div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + </nuxt-link> - <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> - <svg class="w-6 h-6" viewBox="0 0 24 24"> - <path - fill="currentColor" - d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z" - /> - </svg> + <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> + <svg class="w-6 h-6" viewBox="0 0 24 24"> + <path + fill="currentColor" + d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z" + /> + </svg> - <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p> + <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p> - <div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - </nuxt-link> + <div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + </nuxt-link> - <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> - <span class="material-icons text-2xl">record_voice_over</span> + <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> + <span class="material-icons text-2xl">record_voice_over</span> - <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p> + <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p> - <div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - </nuxt-link> + <div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + </nuxt-link> - <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> - <span class="abs-icons icon-podcast text-xl"></span> + <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> + <span class="abs-icons icon-podcast text-xl"></span> - <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSearch }}</p> + <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSearch }}</p> - <div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - </nuxt-link> + <div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + </nuxt-link> - <nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> - <span class="material-icons-outlined text-xl">album</span> + <nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> + <span class="material-icons-outlined text-xl">album</span> - <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p> + <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p> - <div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - </nuxt-link> + <div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + </nuxt-link> - <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> - <span class="material-icons text-2xl">file_download</span> + <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> + <span class="material-icons text-2xl">file_download</span> - <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p> + <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p> - <div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - </nuxt-link> + <div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + </nuxt-link> - <nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'"> - <span class="material-icons text-2xl">warning</span> + <nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'"> + <span class="material-icons text-2xl">warning</span> - <p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p> + <p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p> - <div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> - <div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center"> - <p class="text-xs font-mono pb-0.5">{{ numIssues }}</p> - </div> - </nuxt-link> + <div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> + <div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center"> + <p class="text-xs font-mono pb-0.5">{{ numIssues }}</p> + </div> + </nuxt-link> + </div> - <div class="w-full h-12 px-1 py-2 border-t border-black border-opacity-20 absolute left-0" :style="{ bottom: streamLibraryItem ? '240px' : '65px' }"> + <div class="w-full h-12 px-1 py-2 border-t border-black/20 bg-bg absolute left-0" :style="{ bottom: streamLibraryItem ? '224px' : '65px' }"> <p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1" @click="clickChangelog">v{{ $config.version }}</p> <a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a> <p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p> @@ -235,3 +237,12 @@ export default { mounted() {} } </script> + +<style> +#siderail-buttons-container { + max-height: calc(100vh - 64px - 48px); +} +#siderail-buttons-container.player-open { + max-height: calc(100vh - 64px - 48px - 160px); +} +</style> \ No newline at end of file diff --git a/client/components/widgets/CloseButton.vue b/client/components/widgets/CloseButton.vue deleted file mode 100644 index a9a61e1ba..000000000 --- a/client/components/widgets/CloseButton.vue +++ /dev/null @@ -1,33 +0,0 @@ -<template> - <button class="bg-error text-white px-2 py-1 shadow-md" @click="$emit('click', $event)">Cancel</button> -</template> - -<script> -export default { - data() { - return {} - }, - computed: {}, - methods: {}, - mounted() {} -} -</script> - -<style> -.Vue-Toastification__close-button.cancel-scan-btn { - background-color: rgb(255, 82, 82); - color: white; - font-size: 0.9rem; - opacity: 1; - padding: 0px 10px; - border-radius: 6px; - font-weight: normal; - font-family: 'Open Sans'; - margin-left: 10px; - opacity: 0.3; -} -.Vue-Toastification__close-button.cancel-scan-btn:hover { - background-color: rgb(235, 65, 65); - opacity: 1; -} -</style> \ No newline at end of file diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 4f5e0feaa..df8f754a7 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -25,8 +25,6 @@ </template> <script> -import CloseButton from '@/components/widgets/CloseButton' - export default { middleware: 'authenticated', data() { From 976ae502bbbf42a24c6c7ce1a3f7a46b7d9ec73d Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Mon, 23 Oct 2023 21:48:34 +0000 Subject: [PATCH 083/208] Fix incorrect subpath checks --- server/Watcher.js | 6 +++--- server/utils/fileUtils.js | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/server/Watcher.js b/server/Watcher.js index 3ce6a5f5d..f348ce8e6 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -6,7 +6,7 @@ const LibraryScanner = require('./scanner/LibraryScanner') const Task = require('./objects/Task') const TaskManager = require('./managers/TaskManager') -const { filePathToPOSIX } = require('./utils/fileUtils') +const { filePathToPOSIX, isSameOrSubPath } = require('./utils/fileUtils') /** * @typedef PendingFileUpdate @@ -183,7 +183,7 @@ class FolderWatcher extends EventEmitter { } // Get file folder - const folder = libwatcher.folders.find(fold => path.startsWith(filePathToPOSIX(fold.fullPath))) + const folder = libwatcher.folders.find(fold => isSameOrSubPath(fold.fullPath, path)) if (!folder) { Logger.error(`[Watcher] New file folder not found in library "${libwatcher.name}" with path "${path}"`) return @@ -233,7 +233,7 @@ class FolderWatcher extends EventEmitter { checkShouldIgnorePath(path) { return !!this.ignoreDirs.find(dirpath => { - return filePathToPOSIX(path).startsWith(dirpath) + return isSameOrSubPath(dirpath, path) }) } diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 4df264004..7ee16d56c 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -19,6 +19,18 @@ const filePathToPOSIX = (path) => { } module.exports.filePathToPOSIX = filePathToPOSIX +function isSameOrSubPath(parentPath, childPath) { + parentPath = filePathToPOSIX(parentPath) + childPath = filePathToPOSIX(childPath) + if (parentPath === childPath) return true + const relativePath = Path.relative(parentPath, childPath) + return ( + relativePath === '' // Same path (e.g. parentPath = '/a/b/', childPath = '/a/b') + || !relativePath.startsWith('..') && !Path.isAbsolute(relativePath) // Sub path + ) +} +module.exports.isSameOrSubPath = isSameOrSubPath + async function getFileStat(path) { try { var stat = await fs.stat(path) From 9a477a92705f5e3f31df5e6959b0956f20e98136 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 23 Oct 2023 17:28:59 -0500 Subject: [PATCH 084/208] Add jsdocs --- server/utils/fileUtils.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 7ee16d56c..19735fb70 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -19,6 +19,13 @@ const filePathToPOSIX = (path) => { } module.exports.filePathToPOSIX = filePathToPOSIX +/** + * Check path is a child of or equal to another path + * + * @param {string} parentPath + * @param {string} childPath + * @returns {boolean} + */ function isSameOrSubPath(parentPath, childPath) { parentPath = filePathToPOSIX(parentPath) childPath = filePathToPOSIX(childPath) From 32616aa4416b4eac6493191812d8ef0d35919b99 Mon Sep 17 00:00:00 2001 From: MxMarx <ruby.e.marx@gmail.com> Date: Mon, 23 Oct 2023 20:37:51 -0700 Subject: [PATCH 085/208] show a modal with cover images when clicked --- client/components/app/StreamContainer.vue | 2 +- client/components/covers/BookCover.vue | 19 ++++++++++++++++++- client/pages/item/_id/index.vue | 8 ++++---- client/store/globals.js | 4 ++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index 1aecbf4ee..3439910fd 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -2,7 +2,7 @@ <div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2"> <div id="videoDock" /> <nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-2 top-2 md:left-4 cursor-pointer"> - <covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" /> + <covers-book-cover :expand-on-click="true" :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" /> </nuxt-link> <div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'"> <div class="min-w-0"> diff --git a/client/components/covers/BookCover.vue b/client/components/covers/BookCover.vue index be39ae3ce..810baa439 100644 --- a/client/components/covers/BookCover.vue +++ b/client/components/covers/BookCover.vue @@ -5,7 +5,14 @@ <div class="absolute cover-bg" ref="coverBg" /> </div> - <img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" /> + <img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" draggable="false" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" @click="clickCover" /> + + <modals-modal v-if="libraryItem && expandOnClick" v-model="showImageModal" name="cover" :width="'90%'" :height="'90%'" :contentMarginTop="0"> + <div class="w-full h-full" @click="showImageModal = false"> + <img loading="lazy" :src="rawCoverUrl" class="w-full h-full z-10 object-scale-down" /> + </div> + </modals-modal> + <div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center"> <p class="text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p> <div class="absolute top-2 right-2"> @@ -43,10 +50,12 @@ export default { type: Number, default: 120 }, + expandOnClick: Boolean, bookCoverAspectRatio: Number }, data() { return { + showImageModal: false, loading: true, imageFailed: false, showCoverBg: false, @@ -102,6 +111,11 @@ export default { var store = this.$store || this.$nuxt.$store return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl) }, + rawCoverUrl() { + if (!this.libraryItem) return null + var store = this.$store || this.$nuxt.$store + return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, null, true) + }, cover() { return this.media.coverPath || this.placeholderUrl }, @@ -132,6 +146,9 @@ export default { } }, methods: { + clickCover() { + this.showImageModal = true + }, setCoverBg() { if (this.$refs.coverBg) { this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")` diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 0f4f17b24..eff240f70 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -3,21 +3,21 @@ <div class="w-full h-full overflow-y-auto px-2 py-6 lg:p-8"> <div class="flex flex-col lg:flex-row max-w-6xl mx-auto"> <div class="w-full flex justify-center lg:block lg:w-52" style="min-width: 208px"> - <div class="relative" style="height: fit-content"> - <covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> + <div class="relative group" style="height: fit-content"> + <covers-book-cover class="relative group-hover:brightness-75 transition" :expand-on-click="true" :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <!-- Item Progress Bar --> <div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div> <!-- Item Cover Overlay --> - <div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent> + <div class="absolute top-0 left-0 w-full h-full z-10 pointer-events-none"> <div v-show="showPlayButton && !isStreaming" class="h-full flex items-center justify-center pointer-events-none"> <div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="playItem"> <span class="material-icons text-4xl">play_circle_filled</span> </div> </div> - <span class="absolute bottom-2.5 right-2.5 z-10 material-icons text-lg cursor-pointer text-white text-opacity-75 hover:text-opacity-100 hover:scale-110 transform duration-200" @click="showEditCover">edit</span> + <span class="absolute bottom-2.5 right-2.5 z-10 material-icons text-lg cursor-pointer text-white text-opacity-75 hover:text-opacity-100 hover:scale-110 transform duration-200 pointer-events-auto" @click="showEditCover">edit</span> </div> </div> </div> diff --git a/client/store/globals.js b/client/store/globals.js index a202d6855..44b35f88d 100644 --- a/client/store/globals.js +++ b/client/store/globals.js @@ -80,7 +80,7 @@ export const state = () => ({ }) export const getters = { - getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = null) => { + getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = null, raw = false) => { if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg` if (!libraryItem) return placeholder const media = libraryItem.media @@ -94,7 +94,7 @@ export const getters = { const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers if (process.env.NODE_ENV !== 'production') { // Testing - return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}` + return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}` } return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}` From e054b9a54ca392930901695e2a74b2e306c7a5b0 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Tue, 24 Oct 2023 13:35:43 +0000 Subject: [PATCH 086/208] Add API to update a path on a watched library folder --- server/controllers/MiscController.js | 48 ++++++++++++++++++++++++++++ server/routers/ApiRouter.js | 1 + 2 files changed, 49 insertions(+) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index ffa4e2c2a..fb6124df5 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -527,6 +527,54 @@ class MiscController { }) } + /** + * POST: /api/watcher/update + * Update a watch path + * Req.body { libraryId, path, type, [oldPath] } + * type = add, unlink, rename + * oldPath = required only for rename + * @param {*} req + * @param {*} res + */ + updateWatchedPath(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user attempted to updateWatchedPath`) + return res.sendStatus(404) + } + + const libraryId = req.body.libraryId + const path = req.body.path + const type = req.body.type + if (!libraryId || !path || !type) { + Logger.error(`[MiscController] Invalid request body for updateWatchedPath. libraryId: "${libraryId}", path: "${path}", type: "${type}"`) + return res.sendStatus(400) + } + + switch (type) { + case 'add': + this.watcher.onNewFile(libraryId, path) + break; + case 'unlink': + this.watcher.onFileRemoved(libraryId, path) + break; + case 'rename': + const oldPath = req.body.oldPath + if (!oldPath) { + Logger.error(`[MiscController] Invalid request body for updateWatchedPath. oldPath is required for rename.`) + return res.sendStatus(400) + } + this.watcher.onRename(libraryId, oldPath, path) + break; + default: + Logger.error(`[MiscController] Invalid type for updateWatchedPath. type: "${type}"`) + return res.sendStatus(400) + } + + res.sendStatus(200) + + } + + validateCronExpression(req, res) { const expression = req.body.expression if (!expression) { diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index b40c3d801..c4ac0327e 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -308,6 +308,7 @@ class ApiRouter { this.router.post('/genres/rename', MiscController.renameGenre.bind(this)) this.router.delete('/genres/:genre', MiscController.deleteGenre.bind(this)) this.router.post('/validate-cron', MiscController.validateCronExpression.bind(this)) + this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this)) } async getDirectories(dir, relpath, excludedDirs, level = 0) { From ef1cdf6ad231b5fefff8b83915cfacffd84db945 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 24 Oct 2023 17:04:54 -0500 Subject: [PATCH 087/208] Fix:Only show authors with books for users #2250 --- server/controllers/LibraryController.js | 2 +- server/utils/queries/libraryFilters.js | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 10a77b2a4..d20902701 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -621,7 +621,7 @@ class LibraryController { model: Database.bookModel, attributes: ['id', 'tags', 'explicit'], where: bookWhere, - required: false, + required: !req.user.isAdminOrUp, // Only show authors with 0 books for admin users or up through: { attributes: [] } diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index 6ba6ec5e0..785124a96 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -308,6 +308,8 @@ module.exports = { async getNewestAuthors(library, user, limit) { if (library.mediaType !== 'book') return { authors: [], count: 0 } + const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(user) + const { rows: authors, count } = await Database.authorModel.findAndCountAll({ where: { libraryId: library.id, @@ -315,9 +317,15 @@ module.exports = { [Sequelize.Op.gte]: new Date(new Date() - (60 * 24 * 60 * 60 * 1000)) // 60 days ago } }, + replacements, include: { - model: Database.bookAuthorModel, - required: true // Must belong to a book + model: Database.bookModel, + attributes: ['id', 'tags', 'explicit'], + where: bookWhere, + required: true, // Must belong to a book + through: { + attributes: [] + } }, limit, distinct: true, @@ -328,7 +336,7 @@ module.exports = { return { authors: authors.map((au) => { - const numBooks = au.bookAuthors?.length || 0 + const numBooks = au.books.length || 0 return au.getOldAuthor().toJSONExpanded(numBooks) }), count From 8dc44901699763052db295321e0adbb4eeec0798 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 25 Oct 2023 16:53:53 -0500 Subject: [PATCH 088/208] Fix:Watcher waits for files to finish transferring before scanning #1362 #2248 --- server/Watcher.js | 90 +++++++++++++++++++++++++++++++++------ server/utils/fileUtils.js | 37 +++++++++------- 2 files changed, 97 insertions(+), 30 deletions(-) diff --git a/server/Watcher.js b/server/Watcher.js index f348ce8e6..99318a7e2 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -6,7 +6,7 @@ const LibraryScanner = require('./scanner/LibraryScanner') const Task = require('./objects/Task') const TaskManager = require('./managers/TaskManager') -const { filePathToPOSIX, isSameOrSubPath } = require('./utils/fileUtils') +const { filePathToPOSIX, isSameOrSubPath, getFileMTimeMs } = require('./utils/fileUtils') /** * @typedef PendingFileUpdate @@ -29,6 +29,8 @@ class FolderWatcher extends EventEmitter { /** @type {Task} */ this.pendingTask = null + this.filesBeingAdded = new Set() + /** @type {string[]} */ this.ignoreDirs = [] /** @type {string[]} */ @@ -64,14 +66,13 @@ class FolderWatcher extends EventEmitter { }) watcher .on('add', (path) => { - this.onNewFile(library.id, path) + this.onFileAdded(library.id, filePathToPOSIX(path)) }).on('change', (path) => { // This is triggered from metadata changes, not what we want - // this.onFileUpdated(path) }).on('unlink', path => { - this.onFileRemoved(library.id, path) + this.onFileRemoved(library.id, filePathToPOSIX(path)) }).on('rename', (path, pathNext) => { - this.onRename(library.id, path, pathNext) + this.onFileRename(library.id, filePathToPOSIX(path), filePathToPOSIX(pathNext)) }).on('error', (error) => { Logger.error(`[Watcher] ${error}`) }).on('ready', () => { @@ -137,14 +138,31 @@ class FolderWatcher extends EventEmitter { return this.libraryWatchers.map(lib => lib.watcher.close()) } - onNewFile(libraryId, path) { + /** + * Watcher detected file added + * + * @param {string} libraryId + * @param {string} path + */ + onFileAdded(libraryId, path) { if (this.checkShouldIgnorePath(path)) { return } Logger.debug('[Watcher] File Added', path) this.addFileUpdate(libraryId, path, 'added') + + if (!this.filesBeingAdded.has(path)) { + this.filesBeingAdded.add(path) + this.waitForFileToAdd(path) + } } + /** + * Watcher detected file removed + * + * @param {string} libraryId + * @param {string} path + */ onFileRemoved(libraryId, path) { if (this.checkShouldIgnorePath(path)) { return @@ -153,11 +171,13 @@ class FolderWatcher extends EventEmitter { this.addFileUpdate(libraryId, path, 'deleted') } - onFileUpdated(path) { - Logger.debug('[Watcher] Updated File', path) - } - - onRename(libraryId, pathFrom, pathTo) { + /** + * Watcher detected file renamed + * + * @param {string} libraryId + * @param {string} path + */ + onFileRename(libraryId, pathFrom, pathTo) { if (this.checkShouldIgnorePath(pathTo)) { return } @@ -166,13 +186,41 @@ class FolderWatcher extends EventEmitter { } /** - * File update detected from watcher + * Get mtimeMs from an added file every second until it is no longer changing + * Times out after 180s + * + * @param {string} path + * @param {number} [lastMTimeMs=0] + * @param {number} [loop=0] + */ + async waitForFileToAdd(path, lastMTimeMs = 0, loop = 0) { + // Safety to catch infinite loop (180s) + if (loop >= 180) { + Logger.warn(`[Watcher] Waiting to add file at "${path}" timeout (loop ${loop}) - proceeding`) + return this.filesBeingAdded.delete(path) + } + + const mtimeMs = await getFileMTimeMs(path) + if (mtimeMs === lastMTimeMs) { + if (lastMTimeMs) Logger.debug(`[Watcher] File finished adding at "${path}"`) + return this.filesBeingAdded.delete(path) + } + if (lastMTimeMs % 5 === 0) { + Logger.debug(`[Watcher] Waiting to add file at "${path}". mtimeMs=${mtimeMs} lastMTimeMs=${lastMTimeMs} (loop ${loop})`) + } + // Wait 1 second + await new Promise((resolve) => setTimeout(resolve, 1000)) + this.waitForFileToAdd(path, mtimeMs, ++loop) + } + + /** + * Queue file update + * * @param {string} libraryId * @param {string} path * @param {string} type */ addFileUpdate(libraryId, path, type) { - path = filePathToPOSIX(path) if (this.pendingFilePaths.includes(path)) return // Get file library @@ -222,12 +270,26 @@ class FolderWatcher extends EventEmitter { type }) - // Notify server of update after "pendingDelay" + this.handlePendingFileUpdatesTimeout() + } + + /** + * Wait X seconds before notifying scanner that files changed + * reset timer if files are still copying + */ + handlePendingFileUpdatesTimeout() { clearTimeout(this.pendingTimeout) this.pendingTimeout = setTimeout(() => { + // Check that files are not still being added + if (this.pendingFileUpdates.some(pfu => this.filesBeingAdded.has(pfu.path))) { + Logger.debug(`[Watcher] Still waiting for pending files "${[...this.filesBeingAdded].join(', ')}"`) + return this.handlePendingFileUpdatesTimeout() + } + LibraryScanner.scanFilesChanged(this.pendingFileUpdates, this.pendingTask) this.pendingTask = null this.pendingFileUpdates = [] + this.filesBeingAdded.clear() }, this.pendingDelay) } diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 19735fb70..26578f573 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -38,22 +38,14 @@ function isSameOrSubPath(parentPath, childPath) { } module.exports.isSameOrSubPath = isSameOrSubPath -async function getFileStat(path) { +function getFileStat(path) { try { - var stat = await fs.stat(path) - return { - size: stat.size, - atime: stat.atime, - mtime: stat.mtime, - ctime: stat.ctime, - birthtime: stat.birthtime - } + return fs.stat(path) } catch (err) { Logger.error('[fileUtils] Failed to stat', err) - return false + return null } } -module.exports.getFileStat = getFileStat async function getFileTimestampsWithIno(path) { try { @@ -72,12 +64,25 @@ async function getFileTimestampsWithIno(path) { } module.exports.getFileTimestampsWithIno = getFileTimestampsWithIno -async function getFileSize(path) { - var stat = await getFileStat(path) - if (!stat) return 0 - return stat.size || 0 +/** + * Get file size + * + * @param {string} path + * @returns {Promise<number>} + */ +module.exports.getFileSize = async (path) => { + return (await getFileStat(path))?.size || 0 +} + +/** + * Get file mtimeMs + * + * @param {string} path + * @returns {Promise<number>} epoch timestamp + */ +module.exports.getFileMTimeMs = async (path) => { + return (await getFileStat(path))?.mtimeMs || 0 } -module.exports.getFileSize = getFileSize /** * From 24228b442419109521f1884cbf713a1b30f1737e Mon Sep 17 00:00:00 2001 From: MxMarx <ruby.e.marx@gmail.com> Date: Thu, 26 Oct 2023 02:01:40 -0700 Subject: [PATCH 089/208] Option to change the font family in epub viewer --- client/components/readers/EpubReader.vue | 2 ++ client/components/readers/Reader.vue | 41 +++++++++++++++++------- client/strings/da.json | 1 + client/strings/de.json | 1 + client/strings/en-us.json | 1 + client/strings/es.json | 1 + client/strings/fr.json | 1 + client/strings/gu.json | 1 + client/strings/hi.json | 1 + client/strings/lt.json | 1 + client/strings/nl.json | 1 + client/strings/no.json | 1 + client/strings/pl.json | 1 + client/strings/ru.json | 1 + client/strings/zh-cn.json | 1 + 15 files changed, 45 insertions(+), 11 deletions(-) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index fba30ec9f..7cc3c33af 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -42,6 +42,7 @@ export default { rendition: null, ereaderSettings: { theme: 'dark', + font: 'serif', fontScale: 100, lineSpacing: 115, spread: 'auto' @@ -130,6 +131,7 @@ export default { const fontScale = settings.fontScale || 100 this.rendition.themes.fontSize(`${fontScale}%`) + this.rendition.themes.font(settings.font) this.rendition.spread(settings.spread || 'auto') }, prev() { diff --git a/client/components/readers/Reader.vue b/client/components/readers/Reader.vue index 120bb400a..569ff84f7 100644 --- a/client/components/readers/Reader.vue +++ b/client/components/readers/Reader.vue @@ -63,7 +63,13 @@ <div class="w-40"> <p class="text-lg">{{ $strings.LabelTheme }}:</p> </div> - <ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" /> + <ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems.theme" @input="settingsUpdated" /> + </div> + <div class="flex items-center mb-4"> + <div class="w-40"> + <p class="text-lg">{{ $strings.LabelFontFamily }}:</p> + </div> + <ui-toggle-btns v-model="ereaderSettings.font" :items="themeItems.font" @input="settingsUpdated" /> </div> <div class="flex items-center mb-4"> <div class="w-40"> @@ -103,6 +109,7 @@ export default { showSettings: false, ereaderSettings: { theme: 'dark', + font: 'serif', fontScale: 100, lineSpacing: 115, spread: 'auto' @@ -142,16 +149,28 @@ export default { ] }, themeItems() { - return [ - { - text: this.$strings.LabelThemeDark, - value: 'dark' - }, - { - text: this.$strings.LabelThemeLight, - value: 'light' - } - ] + return { + theme: [ + { + text: this.$strings.LabelThemeDark, + value: 'dark' + }, + { + text: this.$strings.LabelThemeLight, + value: 'light' + } + ], + font: [ + { + text: 'Sans', + value: 'sans-serif', + }, + { + text: 'Serif', + value: 'serif', + } + ] + } }, componentName() { if (this.ebookType === 'epub') return 'readers-epub-reader' diff --git a/client/strings/da.json b/client/strings/da.json index adf138a1c..3197cc3c6 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -260,6 +260,7 @@ "LabelFinished": "Færdig", "LabelFolder": "Mappe", "LabelFolders": "Mapper", + "LabelFontFamily": "Fontfamilie", "LabelFontScale": "Skriftstørrelse", "LabelFormat": "Format", "LabelGenre": "Genre", diff --git a/client/strings/de.json b/client/strings/de.json index a072a549f..942cad8be 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -260,6 +260,7 @@ "LabelFinished": "beendet", "LabelFolder": "Ordner", "LabelFolders": "Verzeichnisse", + "LabelFontFamily": "Schriftfamilie", "LabelFontScale": "Schriftgröße", "LabelFormat": "Format", "LabelGenre": "Kategorie", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 24d077263..9e69aa4e6 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -260,6 +260,7 @@ "LabelFinished": "Finished", "LabelFolder": "Folder", "LabelFolders": "Folders", + "LabelFontFamily": "Font family", "LabelFontScale": "Font scale", "LabelFormat": "Format", "LabelGenre": "Genre", diff --git a/client/strings/es.json b/client/strings/es.json index 4b37139db..b04815ab8 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -260,6 +260,7 @@ "LabelFinished": "Terminado", "LabelFolder": "Carpeta", "LabelFolders": "Carpetas", + "LabelFontFamily": "Familia tipográfica", "LabelFontScale": "Tamaño de Fuente", "LabelFormat": "Formato", "LabelGenre": "Genero", diff --git a/client/strings/fr.json b/client/strings/fr.json index 28bdf743f..11fa14680 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -260,6 +260,7 @@ "LabelFinished": "Fini(e)", "LabelFolder": "Dossier", "LabelFolders": "Dossiers", + "LabelFontFamily": "Famille de polices", "LabelFontScale": "Taille de la police de caractère", "LabelFormat": "Format", "LabelGenre": "Genre", diff --git a/client/strings/gu.json b/client/strings/gu.json index 8593a95d8..b3de487a6 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -260,6 +260,7 @@ "LabelFinished": "Finished", "LabelFolder": "Folder", "LabelFolders": "Folders", + "LabelFontFamily": "ફોન્ટ કુટુંબ", "LabelFontScale": "Font scale", "LabelFormat": "Format", "LabelGenre": "Genre", diff --git a/client/strings/hi.json b/client/strings/hi.json index 82d25986c..d05c1e856 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -260,6 +260,7 @@ "LabelFinished": "Finished", "LabelFolder": "Folder", "LabelFolders": "Folders", + "LabelFontFamily": "फुहारा परिवार", "LabelFontScale": "Font scale", "LabelFormat": "Format", "LabelGenre": "Genre", diff --git a/client/strings/lt.json b/client/strings/lt.json index dee54e124..0623a7abc 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -260,6 +260,7 @@ "LabelFinished": "Baigta", "LabelFolder": "Aplankas", "LabelFolders": "Aplankai", + "LabelFontFamily": "Famiglia di font", "LabelFontScale": "Šrifto mastelis", "LabelFormat": "Formatas", "LabelGenre": "Žanras", diff --git a/client/strings/nl.json b/client/strings/nl.json index 62696dce1..659e3ec56 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -260,6 +260,7 @@ "LabelFinished": "Voltooid", "LabelFolder": "Map", "LabelFolders": "Mappen", + "LabelFontFamily": "Lettertypefamilie", "LabelFontScale": "Lettertype schaal", "LabelFormat": "Formaat", "LabelGenre": "Genre", diff --git a/client/strings/no.json b/client/strings/no.json index dc7685ee7..5bf537f23 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -260,6 +260,7 @@ "LabelFinished": "Fullført", "LabelFolder": "Mappe", "LabelFolders": "Mapper", + "LabelFontFamily": "Fontfamilie", "LabelFontScale": "Font størrelse", "LabelFormat": "Format", "LabelGenre": "Sjanger", diff --git a/client/strings/pl.json b/client/strings/pl.json index c4fb50f8b..16a0970ba 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -260,6 +260,7 @@ "LabelFinished": "Zakończone", "LabelFolder": "Folder", "LabelFolders": "Foldery", + "LabelFontFamily": "Rodzina czcionek", "LabelFontScale": "Font scale", "LabelFormat": "Format", "LabelGenre": "Gatunek", diff --git a/client/strings/ru.json b/client/strings/ru.json index 69868bca1..478ac33a5 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -260,6 +260,7 @@ "LabelFinished": "Закончен", "LabelFolder": "Папка", "LabelFolders": "Папки", + "LabelFontFamily": "Семейство шрифтов", "LabelFontScale": "Масштаб шрифта", "LabelFormat": "Формат", "LabelGenre": "Жанр", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 219e861ad..ded2c9e22 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -260,6 +260,7 @@ "LabelFinished": "已听完", "LabelFolder": "文件夹", "LabelFolders": "文件夹", + "LabelFontFamily": "字体系列", "LabelFontScale": "字体比例", "LabelFormat": "编码格式", "LabelGenre": "流派", From 0c23da7b028dbea3978949ad3863360c2883e8c7 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 26 Oct 2023 16:31:47 -0500 Subject: [PATCH 090/208] Add missing translations --- client/strings/hr.json | 1 + client/strings/it.json | 1 + 2 files changed, 2 insertions(+) diff --git a/client/strings/hr.json b/client/strings/hr.json index e9a323eee..32213095a 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -260,6 +260,7 @@ "LabelFinished": "Finished", "LabelFolder": "Folder", "LabelFolders": "Folderi", + "LabelFontFamily": "Font family", "LabelFontScale": "Font scale", "LabelFormat": "Format", "LabelGenre": "Genre", diff --git a/client/strings/it.json b/client/strings/it.json index f73b3ffc6..8de1f4ec0 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -260,6 +260,7 @@ "LabelFinished": "Finita", "LabelFolder": "Cartella", "LabelFolders": "Cartelle", + "LabelFontFamily": "Font family", "LabelFontScale": "Dimensione Font", "LabelFormat": "Formato", "LabelGenre": "Genere", From f9c4dd24574600c317cf0b992de0defde4eca73b Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 26 Oct 2023 16:41:54 -0500 Subject: [PATCH 091/208] Update watcher function calls, add js docs --- server/controllers/MiscController.js | 24 ++++++++++++------------ server/routers/ApiRouter.js | 1 + 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index fb6124df5..f4f1703d3 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -528,14 +528,16 @@ class MiscController { } /** - * POST: /api/watcher/update - * Update a watch path - * Req.body { libraryId, path, type, [oldPath] } - * type = add, unlink, rename - * oldPath = required only for rename - * @param {*} req - * @param {*} res - */ + * POST: /api/watcher/update + * Update a watch path + * Req.body { libraryId, path, type, [oldPath] } + * type = add, unlink, rename + * oldPath = required only for rename + * @this import('../routers/ApiRouter') + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ updateWatchedPath(req, res) { if (!req.user.isAdminOrUp) { Logger.error(`[MiscController] Non-admin user attempted to updateWatchedPath`) @@ -552,7 +554,7 @@ class MiscController { switch (type) { case 'add': - this.watcher.onNewFile(libraryId, path) + this.watcher.onFileAdded(libraryId, path) break; case 'unlink': this.watcher.onFileRemoved(libraryId, path) @@ -563,7 +565,7 @@ class MiscController { Logger.error(`[MiscController] Invalid request body for updateWatchedPath. oldPath is required for rename.`) return res.sendStatus(400) } - this.watcher.onRename(libraryId, oldPath, path) + this.watcher.onFileRename(libraryId, oldPath, path) break; default: Logger.error(`[MiscController] Invalid type for updateWatchedPath. type: "${type}"`) @@ -571,10 +573,8 @@ class MiscController { } res.sendStatus(200) - } - validateCronExpression(req, res) { const expression = req.body.expression if (!expression) { diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index c4ac0327e..41b247165 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -39,6 +39,7 @@ class ApiRouter { this.playbackSessionManager = Server.playbackSessionManager this.abMergeManager = Server.abMergeManager this.backupManager = Server.backupManager + /** @type {import('../Watcher')} */ this.watcher = Server.watcher this.podcastManager = Server.podcastManager this.audioMetadataManager = Server.audioMetadataManager From 5778200c8fafd569dc36626f0f67ced247ab6cc5 Mon Sep 17 00:00:00 2001 From: MxMarx <ruby.e.marx@gmail.com> Date: Fri, 27 Oct 2023 00:14:46 -0700 Subject: [PATCH 092/208] Make epubs searchable --- client/components/readers/EpubReader.vue | 88 ++++++++++++++++++++++-- client/components/readers/Reader.vue | 45 +++++++++--- client/components/ui/TextInput.vue | 1 + 3 files changed, 120 insertions(+), 14 deletions(-) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index 7cc3c33af..11e7bf9ef 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -40,6 +40,7 @@ export default { book: null, /** @type {ePub.Rendition} */ rendition: null, + chapters: [], ereaderSettings: { theme: 'dark', font: 'serif', @@ -68,10 +69,6 @@ export default { hasNext() { return !this.rendition?.location?.atEnd }, - /** @returns {Array<ePub.NavItem>} */ - chapters() { - return this.book?.navigation?.toc || [] - }, userMediaProgress() { if (!this.libraryItemId) return return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId) @@ -146,6 +143,40 @@ export default { if (!this.rendition?.manager) return return this.rendition?.display(href) }, + /** @returns {object} Returns the chapter that the `position` in the book is in */ + findChapterFromPosition(chapters, position) { + let foundChapter + for (let i = 0; i < chapters.length; i++) { + if (position >= chapters[i].start && (!chapters[i + 1] || position < chapters[i + 1].start)) { + foundChapter = chapters[i] + if (chapters[i].subitems && chapters[i].subitems.length > 0) { + return this.findChapterFromPosition(chapters[i].subitems, position, foundChapter) + } + break + } + } + return foundChapter + }, + /** @returns {Array} Returns an array of chapters that only includes chapters with query results */ + async searchBook(query) { + const chapters = structuredClone(await this.chapters) + const searchResults = await Promise.all(this.book.spine.spineItems.map((item) => item.load(this.book.load.bind(this.book)).then(item.find.bind(item, query)).finally(item.unload.bind(item)))) + const mergedResults = [].concat(...searchResults) + + mergedResults.forEach((chapter) => { + chapter.start = this.book.locations.percentageFromCfi(chapter.cfi) + const foundChapter = this.findChapterFromPosition(chapters, chapter.start) + if (foundChapter) foundChapter.searchResults.push(chapter) + }) + + let filteredResults = chapters.filter(function f(o) { + if (o.searchResults.length) return true + if (o.subitems.length) { + return (o.subitems = o.subitems.filter(f)).length + } + }) + return filteredResults + }, keyUp(e) { const rtl = this.book.package.metadata.direction === 'rtl' if ((e.keyCode || e.which) == 37) { @@ -319,6 +350,55 @@ export default { this.checkSaveLocations(reader.book.locations.save()) }) } + this.getChapters() + }) + }, + getChapters() { + // Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759 + const toc = this.book?.navigation?.toc || [] + + const tocTree = [] + + const resolveURL = (url, relativeTo) => { + // see https://github.com/futurepress/epub.js/issues/1084 + // HACK-ish: abuse the URL API a little to resolve the path + // the base needs to be a valid URL, or it will throw a TypeError, + // so we just set a random base URI and remove it later + const base = 'https://example.invalid/' + return new URL(url, base + relativeTo).href.replace(base, '') + } + + const basePath = this.book.packaging.navPath || this.book.packaging.ncxPath + + const createTree = async (toc, parent) => { + const promises = toc.map(async (tocItem, i) => { + const href = resolveURL(tocItem.href, basePath) + const id = href.split('#')[1] + const item = this.book.spine.get(href) + await item.load(this.book.load.bind(this.book)) + const el = id ? item.document.getElementById(id) : item.document.body + + const cfi = item.cfiFromElement(el) + + parent[i] = { + title: tocItem.label.trim(), + subitems: [], + href, + cfi, + start: this.book.locations.percentageFromCfi(cfi), + end: null, // set by flattenChapters() + id: null, // set by flattenChapters() + searchResults: [] + } + + if (tocItem.subitems) { + await createTree(tocItem.subitems, parent[i].subitems) + } + }) + await Promise.all(promises) + } + return createTree(toc, tocTree).then(() => { + this.chapters = tocTree }) }, resize() { diff --git a/client/components/readers/Reader.vue b/client/components/readers/Reader.vue index 569ff84f7..2a7b90cfb 100644 --- a/client/components/readers/Reader.vue +++ b/client/components/readers/Reader.vue @@ -26,9 +26,9 @@ <component v-if="componentName" ref="readerComponent" :is="componentName" :library-item="selectedLibraryItem" :player-open="!!streamLibraryItem" :keep-progress="keepProgress" :file-id="ebookFileId" @touchstart="touchstart" @touchend="touchend" @hook:mounted="readerMounted" /> <!-- TOC side nav --> - <div v-if="tocOpen" class="w-full h-full fixed inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div> + <div v-if="tocOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div> <div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent="toggleToC"> - <div class="p-4 h-full"> + <div class="flex flex-col p-4 h-full"> <div class="flex items-center mb-2"> <button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100"> <span class="material-icons text-2xl">arrow_back</span> @@ -36,13 +36,28 @@ <p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p> </div> - <div class="tocContent"> + <form @submit.prevent="searchBook" @click.stop.prevent> + <ui-text-input clearable ref="input" @submit="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" class="h-8 w-full text-sm flex mb-2" /> + </form> + + <div class="overflow-y-auto"> + <div v-if="isSearching && !this.searchResults.length" class="w-full h-40 justify-center"> + <p class="text-center text-xl py-4">{{ $strings.MessageNoResults }}</p> + </div> + <ul> - <li v-for="chapter in chapters" :key="chapter.id" class="py-1"> - <a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a> + <li v-for="chapter in isSearching ? this.searchResults : chapters" :key="chapter.id" class="py-1"> + <a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.title }}</a> + <div v-for="searchResults in chapter.searchResults" :key="searchResults.cfi" class="text-sm py-1 pl-4"> + <a :href="searchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(searchResults.cfi)">{{ searchResults.excerpt }}</a> + </div> + <ul v-if="chapter.subitems.length"> <li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4"> - <a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(subchapter.href)">{{ subchapter.label }}</a> + <a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(subchapter.href)">{{ subchapter.title }}</a> + <div v-for="subChapterSearchResults in subchapter.searchResults" :key="subChapterSearchResults.cfi" class="text-sm py-1 pl-4"> + <a :href="subChapterSearchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(subChapterSearchResults.cfi)">{{ subChapterSearchResults.excerpt }}</a> + </div> </li> </ul> </li> @@ -105,6 +120,9 @@ export default { touchstartTime: 0, touchIdentifier: null, chapters: [], + isSearching: false, + searchResults: [], + searchQuery: '', tocOpen: false, showSettings: false, ereaderSettings: { @@ -281,6 +299,15 @@ export default { this.close() } }, + async searchBook() { + if (this.searchQuery.length > 1) { + this.searchResults = await this.$refs.readerComponent.searchBook(this.searchQuery) + this.isSearching = true + } else { + this.isSearching = false + this.searchResults = [] + } + }, next() { if (this.$refs.readerComponent?.next) this.$refs.readerComponent.next() }, @@ -359,6 +386,8 @@ export default { }, close() { this.unregisterListeners() + this.isSearching = false + this.searchQuery = '' this.show = false } }, @@ -372,10 +401,6 @@ export default { </script> <style> -.tocContent { - height: calc(100% - 36px); - overflow-y: auto; -} #reader { height: 100%; } diff --git a/client/components/ui/TextInput.vue b/client/components/ui/TextInput.vue index c347eea30..56825491f 100644 --- a/client/components/ui/TextInput.vue +++ b/client/components/ui/TextInput.vue @@ -68,6 +68,7 @@ export default { methods: { clear() { this.inputValue = '' + this.$emit('submit') }, focused() { this.isFocused = true From 4229cb7fb6fce179c796b80417be8295fcd8f987 Mon Sep 17 00:00:00 2001 From: MxMarx <ruby.e.marx@gmail.com> Date: Fri, 27 Oct 2023 00:35:28 -0700 Subject: [PATCH 093/208] Added a method to unwrap the chapter list --- client/components/readers/EpubReader.vue | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index 11e7bf9ef..aa11d162e 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -401,6 +401,26 @@ export default { this.chapters = tocTree }) }, + flattenChapters(chapters) { + // Convert the nested epub chapters into something that looks like audiobook chapters for player-ui + const unwrap = (chapters) => { + return chapters.reduce((acc, chapter) => { + return chapter.subitems ? [...acc, chapter, ...unwrap(chapter.subitems)] : [...acc, chapter] + }, []) + } + let flattenedChapters = unwrap(chapters) + + flattenedChapters = flattenedChapters.sort((a, b) => a.start - b.start) + for (let i = 0; i < flattenedChapters.length; i++) { + flattenedChapters[i].id = i + if (i < flattenedChapters.length - 1) { + flattenedChapters[i].end = flattenedChapters[i + 1].start + } else { + flattenedChapters[i].end = 1 + } + } + return flattenedChapters + }, resize() { this.windowWidth = window.innerWidth this.windowHeight = window.innerHeight From 6278bb86651d2148750d92cf87f4de56a187a3b6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 27 Oct 2023 16:51:44 -0500 Subject: [PATCH 094/208] Move raw cover preview to a separate global component, fix item page cover overlay show on hover --- client/components/app/StreamContainer.vue | 6 ++-- client/components/covers/BookCover.vue | 16 ++------- .../modals/RawCoverPreviewModal.vue | 33 +++++++++++++++++++ client/layouts/default.vue | 1 + client/pages/item/_id/index.vue | 4 +-- client/store/globals.js | 9 +++++ 6 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 client/components/modals/RawCoverPreviewModal.vue diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index 3439910fd..e9b6969de 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -1,9 +1,9 @@ <template> <div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2"> <div id="videoDock" /> - <nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-2 top-2 md:left-4 cursor-pointer"> - <covers-book-cover :expand-on-click="true" :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" /> - </nuxt-link> + <div class="absolute left-2 top-2 md:left-4 cursor-pointer"> + <covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" /> + </div> <div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'"> <div class="min-w-0"> <nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate"> diff --git a/client/components/covers/BookCover.vue b/client/components/covers/BookCover.vue index 810baa439..a2a4cc2fc 100644 --- a/client/components/covers/BookCover.vue +++ b/client/components/covers/BookCover.vue @@ -7,12 +7,6 @@ <img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" draggable="false" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" @click="clickCover" /> - <modals-modal v-if="libraryItem && expandOnClick" v-model="showImageModal" name="cover" :width="'90%'" :height="'90%'" :contentMarginTop="0"> - <div class="w-full h-full" @click="showImageModal = false"> - <img loading="lazy" :src="rawCoverUrl" class="w-full h-full z-10 object-scale-down" /> - </div> - </modals-modal> - <div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center"> <p class="text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p> <div class="absolute top-2 right-2"> @@ -55,7 +49,6 @@ export default { }, data() { return { - showImageModal: false, loading: true, imageFailed: false, showCoverBg: false, @@ -111,11 +104,6 @@ export default { var store = this.$store || this.$nuxt.$store return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl) }, - rawCoverUrl() { - if (!this.libraryItem) return null - var store = this.$store || this.$nuxt.$store - return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, null, true) - }, cover() { return this.media.coverPath || this.placeholderUrl }, @@ -147,7 +135,9 @@ export default { }, methods: { clickCover() { - this.showImageModal = true + if (this.expandOnClick && this.libraryItem) { + this.$store.commit('globals/setRawCoverPreviewModal', this.libraryItem.id) + } }, setCoverBg() { if (this.$refs.coverBg) { diff --git a/client/components/modals/RawCoverPreviewModal.vue b/client/components/modals/RawCoverPreviewModal.vue new file mode 100644 index 000000000..b8147aa77 --- /dev/null +++ b/client/components/modals/RawCoverPreviewModal.vue @@ -0,0 +1,33 @@ +<template> + <modals-modal v-model="show" name="cover" :width="'90%'" :height="'90%'" :contentMarginTop="0"> + <div class="w-full h-full" @click="show = false"> + <img loading="lazy" :src="rawCoverUrl" class="w-full h-full z-10 object-scale-down" /> + </div> + </modals-modal> +</template> + +<script> +export default { + data() { + return {} + }, + computed: { + show: { + get() { + return this.$store.state.globals.showRawCoverPreviewModal + }, + set(val) { + this.$store.commit('globals/setShowRawCoverPreviewModal', val) + } + }, + selectedLibraryItemId() { + return this.$store.state.globals.selectedLibraryItemId + }, + rawCoverUrl() { + return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.selectedLibraryItemId, null, true) + } + }, + methods: {}, + mounted() {} +} +</script> \ No newline at end of file diff --git a/client/layouts/default.vue b/client/layouts/default.vue index df8f754a7..c3cc34846 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -19,6 +19,7 @@ <modals-authors-edit-modal /> <modals-batch-quick-match-model /> <modals-rssfeed-open-close-modal /> + <modals-raw-cover-preview-modal /> <prompt-confirm /> <readers-reader /> </div> diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index eff240f70..657d564d2 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -4,13 +4,13 @@ <div class="flex flex-col lg:flex-row max-w-6xl mx-auto"> <div class="w-full flex justify-center lg:block lg:w-52" style="min-width: 208px"> <div class="relative group" style="height: fit-content"> - <covers-book-cover class="relative group-hover:brightness-75 transition" :expand-on-click="true" :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> + <covers-book-cover class="relative group-hover:brightness-75 transition cursor-pointer" expand-on-click :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <!-- Item Progress Bar --> <div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div> <!-- Item Cover Overlay --> - <div class="absolute top-0 left-0 w-full h-full z-10 pointer-events-none"> + <div class="absolute top-0 left-0 w-full h-full z-10 opacity-0 group-hover:opacity-100 pointer-events-none"> <div v-show="showPlayButton && !isStreaming" class="h-full flex items-center justify-center pointer-events-none"> <div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="playItem"> <span class="material-icons text-4xl">play_circle_filled</span> diff --git a/client/store/globals.js b/client/store/globals.js index 44b35f88d..961dd52ee 100644 --- a/client/store/globals.js +++ b/client/store/globals.js @@ -11,6 +11,7 @@ export const state = () => ({ showViewPodcastEpisodeModal: false, showRSSFeedOpenCloseModal: false, showConfirmPrompt: false, + showRawCoverPreviewModal: false, confirmPromptOptions: null, showEditAuthorModal: false, rssFeedEntity: null, @@ -20,6 +21,7 @@ export const state = () => ({ selectedCollection: null, selectedAuthor: null, selectedMediaItems: [], + selectedLibraryItemId: null, isCasting: false, // Actively casting isChromecastInitialized: false, // Script loadeds showBatchQuickMatchModal: false, @@ -156,6 +158,13 @@ export const mutations = { state.confirmPromptOptions = options state.showConfirmPrompt = true }, + setShowRawCoverPreviewModal(state, val) { + state.showRawCoverPreviewModal = val + }, + setRawCoverPreviewModal(state, libraryItemId) { + state.selectedLibraryItemId = libraryItemId + state.showRawCoverPreviewModal = true + }, setEditCollection(state, collection) { state.selectedCollection = collection state.showEditCollectionModal = true From 61f2fb28e09190f7c9b67c4e77355af9576a0ef4 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 28 Oct 2023 13:27:53 -0500 Subject: [PATCH 095/208] Add:Help icon buttons for libraries, rss feeds and users config pages, table add new buttons updated --- client/components/app/SettingsContent.vue | 13 +++---------- client/components/modals/AccountModal.vue | 1 + client/components/tables/UsersTable.vue | 11 +---------- client/pages/config/email.vue | 10 ++++++++-- client/pages/config/libraries.vue | 15 +++++++++++++-- client/pages/config/rss-feeds.vue | 10 +++++++++- client/pages/config/users/index.vue | 16 ++++++++++++++-- client/strings/da.json | 3 +++ client/strings/de.json | 3 +++ client/strings/en-us.json | 3 +++ client/strings/es.json | 3 +++ client/strings/fr.json | 3 +++ client/strings/gu.json | 3 +++ client/strings/hi.json | 3 +++ client/strings/hr.json | 3 +++ client/strings/it.json | 3 +++ client/strings/lt.json | 3 +++ client/strings/nl.json | 3 +++ client/strings/no.json | 3 +++ client/strings/pl.json | 3 +++ client/strings/ru.json | 3 +++ client/strings/zh-cn.json | 3 +++ 22 files changed, 94 insertions(+), 27 deletions(-) diff --git a/client/components/app/SettingsContent.vue b/client/components/app/SettingsContent.vue index 233839e70..c78873e38 100644 --- a/client/components/app/SettingsContent.vue +++ b/client/components/app/SettingsContent.vue @@ -3,9 +3,7 @@ <div class="flex items-center mb-2"> <h1 class="text-xl">{{ headerText }}</h1> - <div v-if="showAddButton" class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clicked"> - <button type="button" class="material-icons" :aria-label="$strings.ButtonAdd + ': ' + headerText" style="font-size: 1.4rem">add</button> - </div> + <slot name="header-items"></slot> </div> <p v-if="description" id="settings-description" class="mb-6 text-gray-200" v-html="description" /> @@ -19,14 +17,9 @@ export default { props: { headerText: String, description: String, - note: String, - showAddButton: Boolean + note: String }, - methods: { - clicked() { - this.$emit('clicked') - } - } + methods: {} } </script> diff --git a/client/components/modals/AccountModal.vue b/client/components/modals/AccountModal.vue index ddad3cd38..bdb8711d7 100644 --- a/client/components/modals/AccountModal.vue +++ b/client/components/modals/AccountModal.vue @@ -329,6 +329,7 @@ export default { init() { this.fetchAllTags() this.isNew = !this.account + if (this.account) { this.newUser = { username: this.account.username, diff --git a/client/components/tables/UsersTable.vue b/client/components/tables/UsersTable.vue index 863012b5d..5494911fb 100644 --- a/client/components/tables/UsersTable.vue +++ b/client/components/tables/UsersTable.vue @@ -52,8 +52,6 @@ </tr> </table> </div> - - <modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" /> </div> </template> @@ -62,8 +60,6 @@ export default { data() { return { users: [], - selectedAccount: null, - showAccountModal: false, isDeletingUser: false } }, @@ -114,13 +110,8 @@ export default { }) } }, - clickAddUser() { - this.selectedAccount = null - this.showAccountModal = true - }, editUser(user) { - this.selectedAccount = user - this.showAccountModal = true + this.$emit('edit', user) }, loadUsers() { this.$axios diff --git a/client/pages/config/email.vue b/client/pages/config/email.vue index 5ae659fad..e161a583c 100644 --- a/client/pages/config/email.vue +++ b/client/pages/config/email.vue @@ -51,8 +51,14 @@ </div> </app-settings-content> - <app-settings-content :header-text="$strings.HeaderEreaderDevices" showAddButton :description="''" @clicked="addNewDeviceClick"> - <table v-if="existingEReaderDevices.length" class="tracksTable my-4"> + <app-settings-content :header-text="$strings.HeaderEreaderDevices" :description="''"> + <template #header-items> + <div class="flex-grow" /> + + <ui-btn color="primary" small @click="addNewDeviceClick">{{ $strings.ButtonAddDevice }}</ui-btn> + </template> + + <table v-if="existingEReaderDevices.length" class="tracksTable mt-4"> <tr> <th class="text-left">{{ $strings.LabelName }}</th> <th class="text-left">{{ $strings.LabelEmail }}</th> diff --git a/client/pages/config/libraries.vue b/client/pages/config/libraries.vue index 73158ab1a..1293ccaba 100644 --- a/client/pages/config/libraries.vue +++ b/client/pages/config/libraries.vue @@ -1,7 +1,18 @@ <template> <div> - <app-settings-content :header-text="$strings.HeaderLibraries" show-add-button @clicked="setShowLibraryModal"> - <tables-library-libraries-table @showLibraryModal="setShowLibraryModal" /> + <app-settings-content :header-text="$strings.HeaderLibraries"> + <template #header-items> + <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2"> + <a href="https://www.audiobookshelf.org/guides/library_creation" target="_blank" class="inline-flex"> + <span class="material-icons text-xl w-5 text-gray-200">help_outline</span> + </a> + </ui-tooltip> + + <div class="flex-grow" /> + + <ui-btn color="primary" small @click="setShowLibraryModal()">{{ $strings.ButtonAddLibrary }}</ui-btn> + </template> + <tables-library-libraries-table @showLibraryModal="setShowLibraryModal" class="pt-2" /> </app-settings-content> <modals-libraries-edit-modal v-model="showLibraryModal" :library="selectedLibrary" /> </div> diff --git a/client/pages/config/rss-feeds.vue b/client/pages/config/rss-feeds.vue index 28dba6704..813e69ec9 100644 --- a/client/pages/config/rss-feeds.vue +++ b/client/pages/config/rss-feeds.vue @@ -1,7 +1,15 @@ <template> <div> <app-settings-content :header-text="$strings.HeaderRSSFeeds"> - <div v-if="feeds.length" class="block max-w-full"> + <template #header-items> + <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2"> + <a href="https://www.audiobookshelf.org/guides/rss_feeds" target="_blank" class="inline-flex"> + <span class="material-icons text-xl w-5 text-gray-200">help_outline</span> + </a> + </ui-tooltip> + </template> + + <div v-if="feeds.length" class="block max-w-full pt-2"> <table class="rssFeedsTable text-xs"> <tr class="bg-primary bg-opacity-40 h-12"> <th class="w-16 min-w-16"></th> diff --git a/client/pages/config/users/index.vue b/client/pages/config/users/index.vue index 482da3ced..a03e26552 100644 --- a/client/pages/config/users/index.vue +++ b/client/pages/config/users/index.vue @@ -1,7 +1,19 @@ <template> <div> - <app-settings-content :header-text="$strings.HeaderUsers" show-add-button @clicked="setShowUserModal"> - <tables-users-table /> + <app-settings-content :header-text="$strings.HeaderUsers"> + <template #header-items> + <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2"> + <a href="https://www.audiobookshelf.org/guides/users" target="_blank" class="inline-flex"> + <span class="material-icons text-xl w-5 text-gray-200">help_outline</span> + </a> + </ui-tooltip> + + <div class="flex-grow" /> + + <ui-btn color="primary" small @click="setShowUserModal()">{{ $strings.ButtonAddUser }}</ui-btn> + </template> + + <tables-users-table class="pt-2" @edit="setShowUserModal" /> </app-settings-content> <modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" /> </div> diff --git a/client/strings/da.json b/client/strings/da.json index 3197cc3c6..cf9f836bb 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Tilføj", "ButtonAddChapters": "Tilføj kapitler", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Tilføj podcasts", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Tilføj din første bibliotek", "ButtonApply": "Anvend", "ButtonApplyChapters": "Anvend kapitler", diff --git a/client/strings/de.json b/client/strings/de.json index 942cad8be..e9242a3ee 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Hinzufügen", "ButtonAddChapters": "Kapitel hinzufügen", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Podcasts hinzufügen", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek", "ButtonApply": "Übernehmen", "ButtonApplyChapters": "Kapitel anwenden", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 9e69aa4e6..bfaac5ea4 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Add", "ButtonAddChapters": "Add Chapters", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Add Podcasts", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Add your first library", "ButtonApply": "Apply", "ButtonApplyChapters": "Apply Chapters", diff --git a/client/strings/es.json b/client/strings/es.json index b04815ab8..ca659fc8e 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Agregar", "ButtonAddChapters": "Agregar Capitulo", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Agregar Podcasts", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Agrega tu Primera Biblioteca", "ButtonApply": "Aplicar", "ButtonApplyChapters": "Aplicar Capítulos", diff --git a/client/strings/fr.json b/client/strings/fr.json index 11fa14680..be6241420 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Ajouter", "ButtonAddChapters": "Ajouter le chapitre", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Ajouter des podcasts", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Ajouter votre première bibliothèque", "ButtonApply": "Appliquer", "ButtonApplyChapters": "Appliquer les chapitres", diff --git a/client/strings/gu.json b/client/strings/gu.json index b3de487a6..eb24cd6a6 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -1,7 +1,10 @@ { "ButtonAdd": "ઉમેરો", "ButtonAddChapters": "પ્રકરણો ઉમેરો", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "પોડકાસ્ટ ઉમેરો", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "તમારી પ્રથમ પુસ્તકાલય ઉમેરો", "ButtonApply": "લાગુ કરો", "ButtonApplyChapters": "પ્રકરણો લાગુ કરો", diff --git a/client/strings/hi.json b/client/strings/hi.json index d05c1e856..4ffa2bd35 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -1,7 +1,10 @@ { "ButtonAdd": "जोड़ें", "ButtonAddChapters": "अध्याय जोड़ें", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "पॉडकास्ट जोड़ें", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "अपनी पहली पुस्तकालय जोड़ें", "ButtonApply": "लागू करें", "ButtonApplyChapters": "अध्यायों में परिवर्तन लागू करें", diff --git a/client/strings/hr.json b/client/strings/hr.json index 32213095a..71090fe19 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Dodaj", "ButtonAddChapters": "Dodaj poglavlja", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Dodaj podcaste", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Dodaj svoju prvu biblioteku", "ButtonApply": "Primijeni", "ButtonApplyChapters": "Primijeni poglavlja", diff --git a/client/strings/it.json b/client/strings/it.json index 8de1f4ec0..88f3c5b75 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Aggiungi", "ButtonAddChapters": "Aggiungi Capitoli", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Aggiungi Podcast", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Aggiungi la tua prima libreria", "ButtonApply": "Applica", "ButtonApplyChapters": "Applica", diff --git a/client/strings/lt.json b/client/strings/lt.json index 0623a7abc..6e85d6895 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Pridėti", "ButtonAddChapters": "Pridėti skyrius", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Pridėti tinklalaides", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Pridėkite savo pirmąją biblioteką", "ButtonApply": "Taikyti", "ButtonApplyChapters": "Taikyti skyrius", diff --git a/client/strings/nl.json b/client/strings/nl.json index 659e3ec56..9391f332f 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Toevoegen", "ButtonAddChapters": "Hoofdstukken toevoegen", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Podcasts toevoegen", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Voeg je eerste bibliotheek toe", "ButtonApply": "Pas toe", "ButtonApplyChapters": "Hoofdstukken toepassen", diff --git a/client/strings/no.json b/client/strings/no.json index 5bf537f23..ac16d3511 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Legg til", "ButtonAddChapters": "Legg til kapittel", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Legg til podcast", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Legg til ditt første bibliotek", "ButtonApply": "Bruk", "ButtonApplyChapters": "Bruk kapittel", diff --git a/client/strings/pl.json b/client/strings/pl.json index 16a0970ba..b92cb894f 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Dodaj", "ButtonAddChapters": "Dodaj rozdziały", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Dodaj podcasty", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Dodaj swoją pierwszą bibliotekę", "ButtonApply": "Zatwierdź", "ButtonApplyChapters": "Zatwierdź rozdziały", diff --git a/client/strings/ru.json b/client/strings/ru.json index 478ac33a5..5574aa9e4 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -1,7 +1,10 @@ { "ButtonAdd": "Добавить", "ButtonAddChapters": "Добавить главы", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "Добавить подкасты", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "Добавьте Вашу первую библиотеку", "ButtonApply": "Применить", "ButtonApplyChapters": "Применить главы", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index ded2c9e22..fa815fab2 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -1,7 +1,10 @@ { "ButtonAdd": "增加", "ButtonAddChapters": "添加章节", + "ButtonAddDevice": "Add Device", + "ButtonAddLibrary": "Add Library", "ButtonAddPodcasts": "添加播客", + "ButtonAddUser": "Add User", "ButtonAddYourFirstLibrary": "添加第一个媒体库", "ButtonApply": "应用", "ButtonApplyChapters": "应用到章节", From 88c794e7102c3a64ec1f22020d3976128d1bd45d Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 28 Oct 2023 13:45:06 -0500 Subject: [PATCH 096/208] Fix:Open RSS feed for series & collections respect prevent indexing option #2047 --- server/objects/Feed.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/objects/Feed.js b/server/objects/Feed.js index 20b1c9087..da856de74 100644 --- a/server/objects/Feed.js +++ b/server/objects/Feed.js @@ -174,7 +174,7 @@ class Feed { this.xml = null } - setFromCollection(userId, slug, collectionExpanded, serverAddress) { + setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) { const feedUrl = `${serverAddress}/feed/${slug}` const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length) @@ -198,6 +198,9 @@ class Feed { this.meta.feedUrl = feedUrl this.meta.link = `${serverAddress}/collection/${collectionExpanded.id}` this.meta.explicit = !!itemsWithTracks.some(li => li.media.metadata.explicit) // explicit if any item is explicit + this.meta.preventIndexing = preventIndexing + this.meta.ownerName = ownerName + this.meta.ownerEmail = ownerEmail this.episodes = [] @@ -244,7 +247,7 @@ class Feed { this.xml = null } - setFromSeries(userId, slug, seriesExpanded, serverAddress) { + setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) { const feedUrl = `${serverAddress}/feed/${slug}` let itemsWithTracks = seriesExpanded.books.filter(libraryItem => libraryItem.media.tracks.length) @@ -272,6 +275,9 @@ class Feed { this.meta.feedUrl = feedUrl this.meta.link = `${serverAddress}/library/${libraryId}/series/${seriesExpanded.id}` this.meta.explicit = !!itemsWithTracks.some(li => li.media.metadata.explicit) // explicit if any item is explicit + this.meta.preventIndexing = preventIndexing + this.meta.ownerName = ownerName + this.meta.ownerEmail = ownerEmail this.episodes = [] From 6dc5b58d8e4df37a3c5a7153b81bc2c8a27506cb Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 28 Oct 2023 14:32:11 -0500 Subject: [PATCH 097/208] Update TOC to not close when clicking on it --- client/components/readers/Reader.vue | 20 ++++++++++++-------- client/components/ui/TextInput.vue | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/client/components/readers/Reader.vue b/client/components/readers/Reader.vue index 2a7b90cfb..5ee85182b 100644 --- a/client/components/readers/Reader.vue +++ b/client/components/readers/Reader.vue @@ -27,7 +27,7 @@ <!-- TOC side nav --> <div v-if="tocOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div> - <div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent="toggleToC"> + <div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent> <div class="flex flex-col p-4 h-full"> <div class="flex items-center mb-2"> <button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100"> @@ -37,7 +37,7 @@ <p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p> </div> <form @submit.prevent="searchBook" @click.stop.prevent> - <ui-text-input clearable ref="input" @submit="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" class="h-8 w-full text-sm flex mb-2" /> + <ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" class="h-8 w-full text-sm flex mb-2" /> </form> <div class="overflow-y-auto"> @@ -47,16 +47,16 @@ <ul> <li v-for="chapter in isSearching ? this.searchResults : chapters" :key="chapter.id" class="py-1"> - <a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.title }}</a> + <a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(chapter.href)">{{ chapter.title }}</a> <div v-for="searchResults in chapter.searchResults" :key="searchResults.cfi" class="text-sm py-1 pl-4"> - <a :href="searchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(searchResults.cfi)">{{ searchResults.excerpt }}</a> + <a :href="searchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(searchResults.cfi)">{{ searchResults.excerpt }}</a> </div> <ul v-if="chapter.subitems.length"> <li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4"> - <a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(subchapter.href)">{{ subchapter.title }}</a> + <a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(subchapter.href)">{{ subchapter.title }}</a> <div v-for="subChapterSearchResults in subchapter.searchResults" :key="subChapterSearchResults.cfi" class="text-sm py-1 pl-4"> - <a :href="subChapterSearchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(subChapterSearchResults.cfi)">{{ subChapterSearchResults.excerpt }}</a> + <a :href="subChapterSearchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(subChapterSearchResults.cfi)">{{ subChapterSearchResults.excerpt }}</a> </div> </li> </ul> @@ -181,11 +181,11 @@ export default { font: [ { text: 'Sans', - value: 'sans-serif', + value: 'sans-serif' }, { text: 'Serif', - value: 'serif', + value: 'serif' } ] } @@ -272,6 +272,10 @@ export default { } }, methods: { + goToChapter(uri) { + this.toggleToC() + this.$refs.readerComponent.goToChapter(uri) + }, readerMounted() { if (this.isEpub) { this.loadEreaderSettings() diff --git a/client/components/ui/TextInput.vue b/client/components/ui/TextInput.vue index 56825491f..5f8716354 100644 --- a/client/components/ui/TextInput.vue +++ b/client/components/ui/TextInput.vue @@ -68,7 +68,7 @@ export default { methods: { clear() { this.inputValue = '' - this.$emit('submit') + this.$emit('clear') }, focused() { this.isFocused = true From 2c9f2e0d68b12378b50d44ab333530d63adfce20 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 28 Oct 2023 15:54:19 -0500 Subject: [PATCH 098/208] Fix podcast episode rss feed search showing all episodes are downloaded --- client/components/modals/podcast/EpisodeFeed.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/modals/podcast/EpisodeFeed.vue b/client/components/modals/podcast/EpisodeFeed.vue index 1378dbe58..4a1b4753d 100644 --- a/client/components/modals/podcast/EpisodeFeed.vue +++ b/client/components/modals/podcast/EpisodeFeed.vue @@ -93,7 +93,7 @@ export default { return this.libraryItem.media.metadata.title || 'Unknown' }, allDownloaded() { - return !this.episodesCleaned.some((episode) => this.getIsEpisodeDownloaded(episode)) + return !this.episodesCleaned.some((episode) => !this.getIsEpisodeDownloaded(episode)) }, episodesSelected() { return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key]) From 225dcdeafdd94206b6a0aebe97d96e1d6260960b Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 28 Oct 2023 16:11:15 -0500 Subject: [PATCH 099/208] Fix:RSS feed parser for episode metadata tags that have attributes #1996 --- server/utils/podcastUtils.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 0e68a0a42..cf1567f91 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -66,7 +66,7 @@ function extractPodcastMetadata(channel) { arrayFields.forEach((key) => { const cleanKey = key.split(':').pop() let value = extractFirstArrayItem(channel, key) - if (value && value['_']) value = value['_'] + if (value?.['_']) value = value['_'] metadata[cleanKey] = value }) return metadata @@ -131,7 +131,9 @@ function extractEpisodeData(item) { const arrayFields = ['title', 'itunes:episodeType', 'itunes:season', 'itunes:episode', 'itunes:author', 'itunes:duration', 'itunes:explicit', 'itunes:subtitle'] arrayFields.forEach((key) => { const cleanKey = key.split(':').pop() - episode[cleanKey] = extractFirstArrayItem(item, key) + let value = extractFirstArrayItem(item, key) + if (value?.['_']) value = value['_'] + episode[cleanKey] = value }) return episode } From 94fd3841aa64ddd054303d03655c62e04f92a05b Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 29 Oct 2023 09:20:50 -0500 Subject: [PATCH 100/208] Update:Notification widget shows green dot indicating unseen completed tasks --- .../components/widgets/NotificationWidget.vue | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/client/components/widgets/NotificationWidget.vue b/client/components/widgets/NotificationWidget.vue index 891c13c39..fd883151c 100644 --- a/client/components/widgets/NotificationWidget.vue +++ b/client/components/widgets/NotificationWidget.vue @@ -9,6 +9,8 @@ <span class="material-icons text-1.5xl" aria-label="Activities" role="button">notifications</span> </ui-tooltip> </div> + <div v-if="showUnseenSuccessIndicator" class="w-2 h-2 rounded-full bg-success pointer-events-none absolute -top-1 -right-0.5" /> + <div v-if="showUnseenSuccessIndicator" class="w-2 h-2 rounded-full bg-success/50 pointer-events-none absolute animate-ping -top-1 -right-0.5" /> </button> <transition name="menu"> <div class="sm:w-80 w-full relative"> @@ -46,7 +48,8 @@ export default { isActive: true }, showMenu: false, - disabled: false + disabled: false, + tasksSeen: [] } }, computed: { @@ -60,12 +63,20 @@ export default { // return just the tasks that are running or failed (or show success) in the last 1 minute const tasks = this.tasks.filter((t) => !t.isFinished || ((t.isFailed || t.showSuccess) && t.finishedAt > new Date().getTime() - 1000 * 60)) || [] return tasks.sort((a, b) => b.startedAt - a.startedAt) + }, + showUnseenSuccessIndicator() { + return this.tasksToShow.some((t) => t.isFinished && !t.isFailed && !this.tasksSeen.includes(t.id)) } }, methods: { clickShowMenu() { if (this.disabled) return this.showMenu = !this.showMenu + if (this.showMenu) { + this.tasksToShow.forEach((t) => { + if (!this.tasksSeen.includes(t.id)) this.tasksSeen.push(t.id) + }) + } }, clickedOutside() { this.showMenu = false @@ -83,9 +94,20 @@ export default { default: return '' } + }, + taskFinished(task) { + // add task as seen if menu is open when it finished + if (this.showMenu && !this.tasksSeen.includes(task.id)) { + this.tasksSeen.push(task.id) + } } }, - mounted() {} + mounted() { + this.$root.socket?.on('task_finished', this.taskFinished) + }, + beforeDestroy() { + this.$root.socket?.off('task_finished', this.taskFinished) + } } </script> From 27497451d9847fc57b7e67b8676f35532e299b2d Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 29 Oct 2023 11:28:34 -0500 Subject: [PATCH 101/208] Add:Ereader device setting to set users that have access #1982 --- .../modals/emails/EReaderDeviceModal.vue | 84 +++++++++++++++++-- client/components/ui/Dropdown.vue | 2 +- client/components/ui/InputDropdown.vue | 12 +-- client/components/ui/MultiSelectDropdown.vue | 40 +++++---- client/strings/da.json | 5 ++ client/strings/de.json | 5 ++ client/strings/en-us.json | 5 ++ client/strings/es.json | 5 ++ client/strings/fr.json | 5 ++ client/strings/gu.json | 5 ++ client/strings/hi.json | 5 ++ client/strings/hr.json | 5 ++ client/strings/it.json | 5 ++ client/strings/lt.json | 5 ++ client/strings/nl.json | 5 ++ client/strings/no.json | 5 ++ client/strings/pl.json | 5 ++ client/strings/ru.json | 5 ++ client/strings/zh-cn.json | 5 ++ server/controllers/EmailController.js | 29 +++++-- server/objects/settings/EmailSettings.js | 67 +++++++++++++-- server/objects/user/User.js | 3 + server/routers/ApiRouter.js | 10 +-- 23 files changed, 267 insertions(+), 55 deletions(-) diff --git a/client/components/modals/emails/EReaderDeviceModal.vue b/client/components/modals/emails/EReaderDeviceModal.vue index 4b6e87cf3..79d80f7c4 100644 --- a/client/components/modals/emails/EReaderDeviceModal.vue +++ b/client/components/modals/emails/EReaderDeviceModal.vue @@ -8,7 +8,7 @@ <form @submit.prevent="submitForm"> <div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300"> <div class="w-full px-3 py-5 md:p-12"> - <div class="flex items-center -mx-1 mb-2"> + <div class="flex items-center -mx-1 mb-4"> <div class="w-full md:w-1/2 px-1"> <ui-text-input-with-label ref="ereaderNameInput" v-model="newDevice.name" :disabled="processing" :label="$strings.LabelName" /> </div> @@ -16,6 +16,14 @@ <ui-text-input-with-label ref="ereaderEmailInput" v-model="newDevice.email" :disabled="processing" :label="$strings.LabelEmail" /> </div> </div> + <div class="flex items-center -mx-1 mb-4"> + <div class="w-full md:w-1/2 px-1"> + <ui-dropdown v-model="newDevice.availabilityOption" :label="$strings.LabelDeviceIsAvailableTo" :items="userAvailabilityOptions" @input="availabilityOptionChanged" /> + </div> + <div class="w-full md:w-1/2 px-1"> + <ui-multi-select-dropdown v-if="newDevice.availabilityOption === 'specificUsers'" v-model="newDevice.users" :label="$strings.HeaderUsers" :items="userOptions" /> + </div> + </div> <div class="flex items-center pt-4"> <div class="flex-grow" /> @@ -45,8 +53,11 @@ export default { processing: false, newDevice: { name: '', - email: '' - } + email: '', + availabilityOption: 'adminAndUp', + users: [] + }, + users: [] } }, watch: { @@ -68,10 +79,55 @@ export default { } }, title() { - return this.ereaderDevice ? 'Create Device' : 'Update Device' + return !this.ereaderDevice ? 'Create Device' : 'Update Device' + }, + userAvailabilityOptions() { + return [ + { + text: this.$strings.LabelAdminUsersOnly, + value: 'adminOrUp' + }, + { + text: this.$strings.LabelAllUsersExcludingGuests, + value: 'userOrUp' + }, + { + text: this.$strings.LabelAllUsersIncludingGuests, + value: 'guestOrUp' + }, + { + text: this.$strings.LabelSelectUsers, + value: 'specificUsers' + } + ] + }, + userOptions() { + return this.users.map((u) => ({ text: u.username, value: u.id })) } }, methods: { + availabilityOptionChanged(option) { + if (option === 'specificUsers' && !this.users.length) { + this.loadUsers() + } + }, + async loadUsers() { + this.processing = true + this.users = await this.$axios + .$get('/api/users') + .then((res) => { + return res.users.sort((a, b) => { + return a.createdAt - b.createdAt + }) + }) + .catch((error) => { + console.error('Failed', error) + return [] + }) + .finally(() => { + this.processing = false + }) + }, submitForm() { this.$refs.ereaderNameInput.blur() this.$refs.ereaderEmailInput.blur() @@ -81,19 +137,27 @@ export default { return } + if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) { + this.$toast.error('Must select at least one user') + return + } + if (this.newDevice.availabilityOption !== 'specificUsers') { + this.newDevice.users = [] + } + this.newDevice.name = this.newDevice.name.trim() this.newDevice.email = this.newDevice.email.trim() if (!this.ereaderDevice) { if (this.existingDevices.some((d) => d.name === this.newDevice.name)) { - this.$toast.error('EReader device with that name already exists') + this.$toast.error('Ereader device with that name already exists') return } this.submitCreate() } else { if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) { - this.$toast.error('EReader device with that name already exists') + this.$toast.error('Ereader device with that name already exists') return } @@ -160,9 +224,17 @@ export default { if (this.ereaderDevice) { this.newDevice.name = this.ereaderDevice.name this.newDevice.email = this.ereaderDevice.email + this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'adminOrUp' + this.newDevice.users = this.ereaderDevice.users || [] + + if (this.newDevice.availabilityOption === 'specificUsers' && !this.users.length) { + this.loadUsers() + } } else { this.newDevice.name = '' this.newDevice.email = '' + this.newDevice.availabilityOption = 'adminOrUp' + this.newDevice.users = [] } } }, diff --git a/client/components/ui/Dropdown.vue b/client/components/ui/Dropdown.vue index 69f04afe2..581554998 100644 --- a/client/components/ui/Dropdown.vue +++ b/client/components/ui/Dropdown.vue @@ -13,7 +13,7 @@ </button> <transition name="menu"> - <ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox"> + <ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox"> <template v-for="item in itemsToShow"> <li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)"> <div class="flex items-center"> diff --git a/client/components/ui/InputDropdown.vue b/client/components/ui/InputDropdown.vue index 1d4018fb7..852aa9977 100644 --- a/client/components/ui/InputDropdown.vue +++ b/client/components/ui/InputDropdown.vue @@ -4,7 +4,7 @@ <div ref="wrapper" class="relative"> <form @submit.prevent="submitForm"> <div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'"> - <input ref="input" v-model="textInput" :disabled="disabled" :readonly="!editable" class="h-full w-full bg-transparent focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" /> + <input ref="input" v-model="textInput" :disabled="disabled" :readonly="!editable" class="h-full w-full bg-transparent focus:outline-none px-1" @focus="inputFocus" @blur="inputBlur" /> </div> </form> @@ -48,8 +48,6 @@ export default { data() { return { isFocused: false, - // currentSearch: null, - typingTimeout: null, textInput: null } }, @@ -83,12 +81,6 @@ export default { } }, methods: { - keydownInput() { - clearTimeout(this.typingTimeout) - this.typingTimeout = setTimeout(() => { - // this.currentSearch = this.textInput - }, 100) - }, setFocus() { if (this.$refs.input && this.editable) this.$refs.input.focus() }, @@ -133,11 +125,9 @@ export default { if (val && !this.items.includes(val)) { this.$emit('newItem', val) } - // this.currentSearch = null }, clickedOption(e, item) { this.textInput = null - // this.currentSearch = null this.input = item if (this.$refs.input) this.$refs.input.blur() } diff --git a/client/components/ui/MultiSelectDropdown.vue b/client/components/ui/MultiSelectDropdown.vue index 3baac5725..7a3c7f002 100644 --- a/client/components/ui/MultiSelectDropdown.vue +++ b/client/components/ui/MultiSelectDropdown.vue @@ -1,5 +1,5 @@ <template> - <div class="w-full" v-click-outside="closeMenu"> + <div class="w-full" v-click-outside="clickOutsideObj"> <p class="px-1 text-sm font-semibold">{{ label }}</p> <div ref="wrapper" class="relative"> <div ref="inputWrapper" style="min-height: 40px" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded-md px-2 py-1 cursor-pointer" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent> @@ -11,23 +11,24 @@ </div> </div> - <ul ref="menu" v-show="showMenu" class="absolute z-60 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> - <template v-for="item in items"> - <li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent> - <div class="flex items-center"> - <span class="font-normal ml-3 block truncate">{{ item.text }}</span> + <transition name="menu"> + <ul ref="menu" v-show="showMenu" class="absolute z-60 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> + <template v-for="item in items"> + <li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent> + <p class="font-normal ml-3 block truncate">{{ item.text }}</p> + + <div v-if="selected.includes(item.value)" class="text-yellow-400 absolute inset-y-0 right-0 my-auto w-5 h-5 mr-3 overflow-hidden"> + <span class="material-icons text-xl">checkmark</span> + </div> + </li> + </template> + <li v-if="!items.length" class="text-gray-50 select-none relative py-2 pr-9" role="option"> + <div class="flex items-center justify-center"> + <span class="font-normal">{{ $strings.MessageNoItems }}</span> </div> - <span v-if="selected.includes(item.value)" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4"> - <span class="material-icons text-xl">checkmark</span> - </span> </li> - </template> - <li v-if="!items.length" class="text-gray-50 select-none relative py-2 pr-9" role="option"> - <div class="flex items-center justify-center"> - <span class="font-normal">{{ $strings.MessageNoItems }}</span> - </div> - </li> - </ul> + </ul> + </transition> </div> </div> </template> @@ -48,7 +49,12 @@ export default { data() { return { showMenu: false, - menu: null + menu: null, + clickOutsideObj: { + handler: this.closeMenu, + events: ['mousedown'], + isActive: true + } } }, computed: { diff --git a/client/strings/da.json b/client/strings/da.json index cf9f836bb..768bb724f 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Tilføj {0} Bøger til Samling", "LabelAddToPlaylist": "Tilføj til Afspilningsliste", "LabelAddToPlaylistBatch": "Tilføj {0} Elementer til Afspilningsliste", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "Alle", "LabelAllUsers": "Alle Brugere", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Allerede i dit bibliotek", "LabelAppend": "Tilføj", "LabelAuthor": "Forfatter", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Fravælg Alle", "LabelDevice": "Enheds", "LabelDeviceInfo": "Enhedsinformation", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Mappe", "LabelDiscFromFilename": "Disk fra Filnavn", "LabelDiscFromMetadata": "Disk fra Metadata", @@ -394,6 +398,7 @@ "LabelSeason": "Sæson", "LabelSelectAllEpisodes": "Vælg alle episoder", "LabelSelectEpisodesShowing": "Vælg {0} episoder vist", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Send e-bog til...", "LabelSequence": "Sekvens", "LabelSeries": "Serie", diff --git a/client/strings/de.json b/client/strings/de.json index e9242a3ee..f7cf8b68e 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu", "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", "LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "Alle", "LabelAllUsers": "Alle Benutzer", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden", "LabelAppend": "Anhängen", "LabelAuthor": "Autor", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Alles abwählen", "LabelDevice": "Gerät", "LabelDeviceInfo": "Geräteinformationen", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Verzeichnis", "LabelDiscFromFilename": "CD aus dem Dateinamen", "LabelDiscFromMetadata": "CD aus den Metadaten", @@ -394,6 +398,7 @@ "LabelSeason": "Staffel", "LabelSelectAllEpisodes": "Alle Episoden auswählen", "LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "E-Book senden an...", "LabelSequence": "Reihenfolge", "LabelSeries": "Serien", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index bfaac5ea4..1366c762a 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylistBatch": "Add {0} Items to Playlist", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "All", "LabelAllUsers": "All Users", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Already in your library", "LabelAppend": "Append", "LabelAuthor": "Author", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Deselect All", "LabelDevice": "Device", "LabelDeviceInfo": "Device Info", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Directory", "LabelDiscFromFilename": "Disc from Filename", "LabelDiscFromMetadata": "Disc from Metadata", @@ -394,6 +398,7 @@ "LabelSeason": "Season", "LabelSelectAllEpisodes": "Select all episodes", "LabelSelectEpisodesShowing": "Select {0} episodes showing", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Send Ebook to...", "LabelSequence": "Sequence", "LabelSeries": "Series", diff --git a/client/strings/es.json b/client/strings/es.json index ca659fc8e..0ac0a9601 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Se Añadieron {0} Libros a la Colección", "LabelAddToPlaylist": "Añadido a la Lista de Reproducción", "LabelAddToPlaylistBatch": "Se Añadieron {0} Artículos a la Lista de Reproducción", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "Todos", "LabelAllUsers": "Todos los Usuarios", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Ya en la Biblioteca", "LabelAppend": "Adjuntar", "LabelAuthor": "Autor", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Deseleccionar Todos", "LabelDevice": "Dispositivo", "LabelDeviceInfo": "Información de Dispositivo", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Directorio", "LabelDiscFromFilename": "Disco a partir del Nombre del Archivo", "LabelDiscFromMetadata": "Disco a partir de Metadata", @@ -394,6 +398,7 @@ "LabelSeason": "Temporada", "LabelSelectAllEpisodes": "Seleccionar todos los episodios", "LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Enviar Ebook a...", "LabelSequence": "Secuencia", "LabelSeries": "Series", diff --git a/client/strings/fr.json b/client/strings/fr.json index be6241420..5ad80723f 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection", "LabelAddToPlaylist": "Ajouter à la liste de lecture", "LabelAddToPlaylistBatch": "{0} éléments ajoutés à la liste de lecture", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "Tout", "LabelAllUsers": "Tous les utilisateurs", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Déjà dans la bibliothèque", "LabelAppend": "Ajouter", "LabelAuthor": "Auteur", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Tout déselectionner", "LabelDevice": "Appareil", "LabelDeviceInfo": "Détail de l’appareil", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Répertoire", "LabelDiscFromFilename": "Disque depuis le fichier", "LabelDiscFromMetadata": "Disque depuis les métadonnées", @@ -394,6 +398,7 @@ "LabelSeason": "Saison", "LabelSelectAllEpisodes": "Sélectionner tous les épisodes", "LabelSelectEpisodesShowing": "Sélectionner {0} episode(s) en cours", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Envoyer le livre numérique à...", "LabelSequence": "Séquence", "LabelSeries": "Séries", diff --git a/client/strings/gu.json b/client/strings/gu.json index eb24cd6a6..d71c9f176 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylistBatch": "Add {0} Items to Playlist", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "All", "LabelAllUsers": "All Users", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Already in your library", "LabelAppend": "Append", "LabelAuthor": "Author", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Deselect All", "LabelDevice": "Device", "LabelDeviceInfo": "Device Info", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Directory", "LabelDiscFromFilename": "Disc from Filename", "LabelDiscFromMetadata": "Disc from Metadata", @@ -394,6 +398,7 @@ "LabelSeason": "Season", "LabelSelectAllEpisodes": "Select all episodes", "LabelSelectEpisodesShowing": "Select {0} episodes showing", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Send Ebook to...", "LabelSequence": "Sequence", "LabelSeries": "Series", diff --git a/client/strings/hi.json b/client/strings/hi.json index 4ffa2bd35..51b2e7629 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylistBatch": "Add {0} Items to Playlist", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "All", "LabelAllUsers": "All Users", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Already in your library", "LabelAppend": "Append", "LabelAuthor": "Author", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Deselect All", "LabelDevice": "Device", "LabelDeviceInfo": "Device Info", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Directory", "LabelDiscFromFilename": "Disc from Filename", "LabelDiscFromMetadata": "Disc from Metadata", @@ -394,6 +398,7 @@ "LabelSeason": "Season", "LabelSelectAllEpisodes": "Select all episodes", "LabelSelectEpisodesShowing": "Select {0} episodes showing", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Send Ebook to...", "LabelSequence": "Sequence", "LabelSeries": "Series", diff --git a/client/strings/hr.json b/client/strings/hr.json index 71090fe19..e04343a09 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylistBatch": "Add {0} Items to Playlist", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "All", "LabelAllUsers": "Svi korisnici", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Already in your library", "LabelAppend": "Append", "LabelAuthor": "Autor", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Odznači sve", "LabelDevice": "Uređaj", "LabelDeviceInfo": "O uređaju", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Direktorij", "LabelDiscFromFilename": "CD iz imena datoteke", "LabelDiscFromMetadata": "CD iz metapodataka", @@ -394,6 +398,7 @@ "LabelSeason": "Sezona", "LabelSelectAllEpisodes": "Select all episodes", "LabelSelectEpisodesShowing": "Select {0} episodes showing", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Send Ebook to...", "LabelSequence": "Sekvenca", "LabelSeries": "Serije", diff --git a/client/strings/it.json b/client/strings/it.json index 88f3c5b75..747d7420c 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta", "LabelAddToPlaylist": "aggiungi alla Playlist", "LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "Tutti", "LabelAllUsers": "Tutti gli Utenti", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Già esistente nella libreria", "LabelAppend": "Appese", "LabelAuthor": "Autore", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Deseleziona Tutto", "LabelDevice": "Dispositivo", "LabelDeviceInfo": "Info Dispositivo", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Elenco", "LabelDiscFromFilename": "Disco dal nome file", "LabelDiscFromMetadata": "Disco dal Metadata", @@ -394,6 +398,7 @@ "LabelSeason": "Stagione", "LabelSelectAllEpisodes": "Seleziona tutti gli Episodi", "LabelSelectEpisodesShowing": "Episodi {0} selezionati ", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Invia ebook a...", "LabelSequence": "Sequenza", "LabelSeries": "Serie", diff --git a/client/strings/lt.json b/client/strings/lt.json index 6e85d6895..ebc6b5588 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Pridėti {0} knygas į kolekciją", "LabelAddToPlaylist": "Pridėti į grojaraštį", "LabelAddToPlaylistBatch": "Pridėti {0} elementus į grojaraštį", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "Visi", "LabelAllUsers": "Visi naudotojai", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Jau yra jūsų bibliotekoje", "LabelAppend": "Pridėti", "LabelAuthor": "Autorius", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Išvalyti pasirinktus", "LabelDevice": "Įrenginys", "LabelDeviceInfo": "Įrenginio informacija", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Katalogas", "LabelDiscFromFilename": "Diskas pagal failo pavadinimą", "LabelDiscFromMetadata": "Diskas pagal metaduomenis", @@ -394,6 +398,7 @@ "LabelSeason": "Sezonas", "LabelSelectAllEpisodes": "Pažymėti visus epizodus", "LabelSelectEpisodesShowing": "Pažymėti {0} rodomus epizodus", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Siųsti e-knygą į...", "LabelSequence": "Seka", "LabelSeries": "Serija", diff --git a/client/strings/nl.json b/client/strings/nl.json index 9391f332f..06aed9045 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "{0} boeken toevoegen aan collectie", "LabelAddToPlaylist": "Toevoegen aan afspeellijst", "LabelAddToPlaylistBatch": "{0} onderdelen toevoegen aan afspeellijst", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "Alle", "LabelAllUsers": "Alle gebruikers", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Reeds in je bibliotheek", "LabelAppend": "Achteraan toevoegen", "LabelAuthor": "Auteur", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Deselecteer alle", "LabelDevice": "Apparaat", "LabelDeviceInfo": "Apparaat info", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Map", "LabelDiscFromFilename": "Schijf uit bestandsnaam", "LabelDiscFromMetadata": "Schijf uit metadata", @@ -394,6 +398,7 @@ "LabelSeason": "Seizoen", "LabelSelectAllEpisodes": "Selecteer alle afleveringen", "LabelSelectEpisodesShowing": "Selecteer {0} afleveringen laten zien", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Stuur ebook naar...", "LabelSequence": "Sequentie", "LabelSeries": "Serie", diff --git a/client/strings/no.json b/client/strings/no.json index ac16d3511..7fcd1c96a 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Legg {0} bøker til samling", "LabelAddToPlaylist": "Legg til i spilleliste", "LabelAddToPlaylistBatch": "Legg {0} enheter til i spilleliste", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "Alle", "LabelAllUsers": "Alle brukere", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Allerede i biblioteket", "LabelAppend": "Legge til", "LabelAuthor": "Forfatter", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Fjern valg", "LabelDevice": "Enhet", "LabelDeviceInfo": "Enhetsinformasjon", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Mappe", "LabelDiscFromFilename": "Disk fra filnavn", "LabelDiscFromMetadata": "Disk fra metadata", @@ -394,6 +398,7 @@ "LabelSeason": "Sesong", "LabelSelectAllEpisodes": "Velg alle episoder", "LabelSelectEpisodesShowing": "Velg {0} episoder vist", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Send Ebok til...", "LabelSequence": "Sekvens", "LabelSeries": "Serier", diff --git a/client/strings/pl.json b/client/strings/pl.json index b92cb894f..dd3c1d4ad 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji", "LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylistBatch": "Add {0} Items to Playlist", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "All", "LabelAllUsers": "Wszyscy użytkownicy", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Already in your library", "LabelAppend": "Append", "LabelAuthor": "Autor", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Odznacz wszystko", "LabelDevice": "Urządzenie", "LabelDeviceInfo": "Informacja o urządzeniu", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Katalog", "LabelDiscFromFilename": "Oznaczenie dysku z nazwy pliku", "LabelDiscFromMetadata": "Oznaczenie dysku z metadanych", @@ -394,6 +398,7 @@ "LabelSeason": "Sezon", "LabelSelectAllEpisodes": "Select all episodes", "LabelSelectEpisodesShowing": "Select {0} episodes showing", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Send Ebook to...", "LabelSequence": "Kolejność", "LabelSeries": "Serie", diff --git a/client/strings/ru.json b/client/strings/ru.json index 5574aa9e4..832ffe8b6 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию", "LabelAddToPlaylist": "Добавить в плейлист", "LabelAddToPlaylistBatch": "Добавить {0} элементов в плейлист", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "Все", "LabelAllUsers": "Все пользователи", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Уже в Вашей библиотеке", "LabelAppend": "Добавить", "LabelAuthor": "Автор", @@ -229,6 +232,7 @@ "LabelDeselectAll": "Снять выделение", "LabelDevice": "Устройство", "LabelDeviceInfo": "Информация об устройстве", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "Каталог", "LabelDiscFromFilename": "Диск из Имени файла", "LabelDiscFromMetadata": "Диск из Метаданных", @@ -394,6 +398,7 @@ "LabelSeason": "Сезон", "LabelSelectAllEpisodes": "Выбрать все эпизоды", "LabelSelectEpisodesShowing": "Выберите {0} эпизодов для показа", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "Отправить e-книгу в...", "LabelSequence": "Последовательность", "LabelSeries": "Серия", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index fa815fab2..5d3de27ad 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -181,8 +181,11 @@ "LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏", "LabelAddToPlaylist": "添加到播放列表", "LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表", + "LabelAdminUsersOnly": "Admin users only", "LabelAll": "全部", "LabelAllUsers": "所有用户", + "LabelAllUsersExcludingGuests": "All users excluding guests", + "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "已存在你的库中", "LabelAppend": "附加", "LabelAuthor": "作者", @@ -229,6 +232,7 @@ "LabelDeselectAll": "全部取消选择", "LabelDevice": "设备", "LabelDeviceInfo": "设备信息", + "LabelDeviceIsAvailableTo": "Device is available to...", "LabelDirectory": "目录", "LabelDiscFromFilename": "从文件名获取光盘", "LabelDiscFromMetadata": "从元数据获取光盘", @@ -394,6 +398,7 @@ "LabelSeason": "季", "LabelSelectAllEpisodes": "选择所有剧集", "LabelSelectEpisodesShowing": "选择正在播放的 {0} 剧集", + "LabelSelectUsers": "Select users", "LabelSendEbookToDevice": "发送电子书到...", "LabelSequence": "序列", "LabelSeries": "系列", diff --git a/server/controllers/EmailController.js b/server/controllers/EmailController.js index fefc23b6c..fcbc49054 100644 --- a/server/controllers/EmailController.js +++ b/server/controllers/EmailController.js @@ -51,32 +51,45 @@ class EmailController { }) } + /** + * Send ebook to device + * User must have access to device and library item + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ async sendEBookToDevice(req, res) { - Logger.debug(`[EmailController] Send ebook to device request for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`) + Logger.debug(`[EmailController] Send ebook to device requested by user "${req.user.username}" for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`) + + const device = Database.emailSettings.getEReaderDevice(req.body.deviceName) + if (!device) { + return res.status(404).send('Ereader device not found') + } + + // Check user has access to device + if (!Database.emailSettings.checkUserCanAccessDevice(device, req.user)) { + return res.sendStatus(403) + } const libraryItem = await Database.libraryItemModel.getOldById(req.body.libraryItemId) if (!libraryItem) { return res.status(404).send('Library item not found') } + // Check user has access to library item if (!req.user.checkCanAccessLibraryItem(libraryItem)) { return res.sendStatus(403) } const ebookFile = libraryItem.media.ebookFile if (!ebookFile) { - return res.status(404).send('EBook file not found') - } - - const device = Database.emailSettings.getEReaderDevice(req.body.deviceName) - if (!device) { - return res.status(404).send('E-reader device not found') + return res.status(404).send('Ebook file not found') } this.emailManager.sendEBookToDevice(ebookFile, device, res) } - middleware(req, res, next) { + adminMiddleware(req, res, next) { if (!req.user.isAdminOrUp) { return res.sendStatus(404) } diff --git a/server/objects/settings/EmailSettings.js b/server/objects/settings/EmailSettings.js index 40648887b..81e31d53d 100644 --- a/server/objects/settings/EmailSettings.js +++ b/server/objects/settings/EmailSettings.js @@ -1,6 +1,14 @@ const Logger = require('../../Logger') const { areEquivalent, copyValue, isNullOrNaN } = require('../../utils') +/** + * @typedef EreaderDeviceObject + * @property {string} name + * @property {string} email + * @property {string} availabilityOption + * @property {string[]} users + */ + // REF: https://nodemailer.com/smtp/ class EmailSettings { constructor(settings = null) { @@ -13,7 +21,7 @@ class EmailSettings { this.testAddress = null this.fromAddress = null - // Array of { name:String, email:String } + /** @type {EreaderDeviceObject[]} */ this.ereaderDevices = [] if (settings) { @@ -57,6 +65,26 @@ class EmailSettings { if (payload.ereaderDevices !== undefined && !Array.isArray(payload.ereaderDevices)) payload.ereaderDevices = undefined + if (payload.ereaderDevices?.length) { + // Validate ereader devices + payload.ereaderDevices = payload.ereaderDevices.map((device) => { + if (!device.name || !device.email) { + Logger.error(`[EmailSettings] Update ereader device is invalid`, device) + return null + } + if (!device.availabilityOption || !['adminOrUp', 'userOrUp', 'guestOrUp', 'specificUsers'].includes(device.availabilityOption)) { + device.availabilityOption = 'adminOrUp' + } + if (device.availabilityOption === 'specificUsers' && !device.users?.length) { + device.availabilityOption = 'adminOrUp' + } + if (device.availabilityOption !== 'specificUsers' && device.users?.length) { + device.users = [] + } + return device + }).filter(d => d) + } + let hasUpdates = false const json = this.toJSON() @@ -88,15 +116,40 @@ class EmailSettings { return payload } - getEReaderDevices(user) { - // Only accessible to admin or up - if (!user.isAdminOrUp) { - return [] + /** + * + * @param {EreaderDeviceObject} device + * @param {import('../user/User')} user + * @returns {boolean} + */ + checkUserCanAccessDevice(device, user) { + let deviceAvailability = device.availabilityOption || 'adminOrUp' + if (deviceAvailability === 'adminOrUp' && user.isAdminOrUp) return true + if (deviceAvailability === 'userOrUp' && (user.isAdminOrUp || user.isUser)) return true + if (deviceAvailability === 'guestOrUp') return true + if (deviceAvailability === 'specificUsers') { + let deviceUsers = device.users || [] + return deviceUsers.includes(user.id) } - - return this.ereaderDevices.map(d => ({ ...d })) + return false } + /** + * Get ereader devices accessible to user + * + * @param {import('../user/User')} user + * @returns {EreaderDeviceObject[]} + */ + getEReaderDevices(user) { + return this.ereaderDevices.filter((device) => this.checkUserCanAccessDevice(device, user)) + } + + /** + * Get ereader device by name + * + * @param {string} deviceName + * @returns {EreaderDeviceObject} + */ getEReaderDevice(deviceName) { return this.ereaderDevices.find(d => d.name === deviceName) } diff --git a/server/objects/user/User.js b/server/objects/user/User.js index a9c9c767b..5192752a3 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -35,6 +35,9 @@ class User { get isAdmin() { return this.type === 'admin' } + get isUser() { + return this.type === 'user' + } get isGuest() { return this.type === 'guest' } diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 41b247165..bb91e9b5e 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -255,11 +255,11 @@ class ApiRouter { // // Email Routes (Admin and up) // - this.router.get('/emails/settings', EmailController.middleware.bind(this), EmailController.getSettings.bind(this)) - this.router.patch('/emails/settings', EmailController.middleware.bind(this), EmailController.updateSettings.bind(this)) - this.router.post('/emails/test', EmailController.middleware.bind(this), EmailController.sendTest.bind(this)) - this.router.post('/emails/ereader-devices', EmailController.middleware.bind(this), EmailController.updateEReaderDevices.bind(this)) - this.router.post('/emails/send-ebook-to-device', EmailController.middleware.bind(this), EmailController.sendEBookToDevice.bind(this)) + this.router.get('/emails/settings', EmailController.adminMiddleware.bind(this), EmailController.getSettings.bind(this)) + this.router.patch('/emails/settings', EmailController.adminMiddleware.bind(this), EmailController.updateSettings.bind(this)) + this.router.post('/emails/test', EmailController.adminMiddleware.bind(this), EmailController.sendTest.bind(this)) + this.router.post('/emails/ereader-devices', EmailController.adminMiddleware.bind(this), EmailController.updateEReaderDevices.bind(this)) + this.router.post('/emails/send-ebook-to-device', EmailController.sendEBookToDevice.bind(this)) // // Search Routes From 2ef11e5ad05406ed5199d0259f9ee35b075bf7fd Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 29 Oct 2023 12:58:00 -0500 Subject: [PATCH 102/208] Version bump v2.5.0 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 6 +++--- package.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 25000ab00..1dc72e4c3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.4.4", + "version": "2.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.4.4", + "version": "2.5.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 21cae1248..c815d388d 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.4.4", + "version": "2.5.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 7178ac984..888c3beb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.4.4", + "version": "2.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.4.4", + "version": "2.5.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", @@ -4704,4 +4704,4 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index f8ea7deed..4bef0e425 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.4.4", + "version": "2.5.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From 9616d996408d079edddfdcb8de29bf38da5e0cbe Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 30 Oct 2023 16:35:41 -0500 Subject: [PATCH 103/208] Fix:Crash when matching with author names ending in ??? by escaping regex strings #2265 --- server/finders/BookFinder.js | 4 ++-- server/utils/index.js | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index a0b64f552..75e5a5f11 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -6,7 +6,7 @@ const Audnexus = require('../providers/Audnexus') const FantLab = require('../providers/FantLab') const AudiobookCovers = require('../providers/AudiobookCovers') const Logger = require('../Logger') -const { levenshteinDistance } = require('../utils/index') +const { levenshteinDistance, escapeRegExp } = require('../utils/index') class BookFinder { constructor() { @@ -201,7 +201,7 @@ class BookFinder { add(title, position = 0) { // if title contains the author, remove it if (this.cleanAuthor) { - const authorRe = new RegExp(`(^| | by |)${this.cleanAuthor}(?= |$)`, "g") + const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g") title = this.bookFinder.cleanAuthorForCompares(title).replace(authorRe, '').trim() } diff --git a/server/utils/index.js b/server/utils/index.js index 84167229a..0377b1731 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -192,4 +192,16 @@ module.exports.asciiOnlyToLowerCase = (str) => { } } return temp +} + +/** + * Escape string used in RegExp + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + * + * @param {string} str + * @returns {string} + */ +module.exports.escapeRegExp = (str) => { + if (typeof str !== 'string') return '' + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } \ No newline at end of file From 3c21e9d4135f3e9f7cd0b111776c2d65a2455035 Mon Sep 17 00:00:00 2001 From: "clement.dufour" <clement.dufour@tutanota.com> Date: Wed, 1 Nov 2023 11:51:39 +0100 Subject: [PATCH 104/208] Update:Simpler content URL in RSS feeds --- server/objects/FeedEpisode.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/objects/FeedEpisode.js b/server/objects/FeedEpisode.js index eeef5379e..b87cf88f5 100644 --- a/server/objects/FeedEpisode.js +++ b/server/objects/FeedEpisode.js @@ -1,3 +1,4 @@ +const Path = require('path') const uuidv4 = require("uuid").v4 const date = require('../libs/dateAndTime') const { secondsToTimestamp } = require('../utils/index') @@ -69,7 +70,8 @@ class FeedEpisode { } setFromPodcastEpisode(libraryItem, serverAddress, slug, episode, meta) { - const contentUrl = `/feed/${slug}/item/${episode.id}/${episode.audioFile.metadata.filename}` + const contentFileExtension = Path.extname(episode.audioFile.metadata.filename) + const contentUrl = `/feed/${slug}/item/${episode.id}/media${contentFileExtension}` const media = libraryItem.media const mediaMetadata = media.metadata @@ -108,7 +110,8 @@ class FeedEpisode { // e.g. Track 1 will have a pub date before Track 2 const audiobookPubDate = date.format(new Date(libraryItem.addedAt + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]') - const contentUrl = `/feed/${slug}/item/${episodeId}/${audioTrack.metadata.filename}` + const contentFileExtension = Path.extname(audioTrack.metadata.filename) + const contentUrl = `/feed/${slug}/item/${episodeId}/media${contentFileExtension}` const media = libraryItem.media const mediaMetadata = media.metadata From 1ae20892536ee467bfe146eabfa3629fd7b37b41 Mon Sep 17 00:00:00 2001 From: "clement.dufour" <clement.dufour@tutanota.com> Date: Wed, 1 Nov 2023 12:11:24 +0100 Subject: [PATCH 105/208] Update:Add cover file extension in RSS feeds --- server/Server.js | 2 +- server/objects/Feed.js | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/server/Server.js b/server/Server.js index d95bd7997..ba63b2bde 100644 --- a/server/Server.js +++ b/server/Server.js @@ -155,7 +155,7 @@ class Server { Logger.info(`[Server] Requesting rss feed ${req.params.slug}`) this.rssFeedManager.getFeed(req, res) }) - router.get('/feed/:slug/cover', (req, res) => { + router.get('/feed/:slug/cover*', (req, res) => { this.rssFeedManager.getFeedCover(req, res) }) router.get('/feed/:slug/item/:episodeId/*', (req, res) => { diff --git a/server/objects/Feed.js b/server/objects/Feed.js index da856de74..08a602aec 100644 --- a/server/objects/Feed.js +++ b/server/objects/Feed.js @@ -1,3 +1,4 @@ +const Path = require('path') const uuidv4 = require("uuid").v4 const FeedMeta = require('./FeedMeta') const FeedEpisode = require('./FeedEpisode') @@ -101,11 +102,13 @@ class Feed { this.serverAddress = serverAddress this.feedUrl = feedUrl + const coverFileExtension = this.coverPath ? Path.extname(media.coverPath) : null + this.meta = new FeedMeta() this.meta.title = mediaMetadata.title this.meta.description = mediaMetadata.description this.meta.author = author - this.meta.imageUrl = media.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png` + this.meta.imageUrl = media.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png` this.meta.feedUrl = feedUrl this.meta.link = `${serverAddress}/item/${libraryItem.id}` this.meta.explicit = !!mediaMetadata.explicit @@ -145,10 +148,12 @@ class Feed { this.entityUpdatedAt = libraryItem.updatedAt this.coverPath = media.coverPath || null + const coverFileExtension = this.coverPath ? Path.extname(media.coverPath) : null + this.meta.title = mediaMetadata.title this.meta.description = mediaMetadata.description this.meta.author = author - this.meta.imageUrl = media.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover` : `${this.serverAddress}/Logo.png` + this.meta.imageUrl = media.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png` this.meta.explicit = !!mediaMetadata.explicit this.meta.type = mediaMetadata.type this.meta.language = mediaMetadata.language @@ -190,11 +195,13 @@ class Feed { this.serverAddress = serverAddress this.feedUrl = feedUrl + const coverFileExtension = this.coverPath ? Path.extname(media.coverPath) : null + this.meta = new FeedMeta() this.meta.title = collectionExpanded.name this.meta.description = collectionExpanded.description || '' this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png` + this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png` this.meta.feedUrl = feedUrl this.meta.link = `${serverAddress}/collection/${collectionExpanded.id}` this.meta.explicit = !!itemsWithTracks.some(li => li.media.metadata.explicit) // explicit if any item is explicit @@ -225,10 +232,12 @@ class Feed { this.entityUpdatedAt = collectionExpanded.lastUpdate this.coverPath = firstItemWithCover?.coverPath || null + const coverFileExtension = this.coverPath ? Path.extname(media.coverPath) : null + this.meta.title = collectionExpanded.name this.meta.description = collectionExpanded.description || '' this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover` : `${this.serverAddress}/Logo.png` + this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png` this.meta.explicit = !!itemsWithTracks.some(li => li.media.metadata.explicit) // explicit if any item is explicit this.episodes = [] @@ -267,11 +276,13 @@ class Feed { this.serverAddress = serverAddress this.feedUrl = feedUrl + const coverFileExtension = this.coverPath ? Path.extname(media.coverPath) : null + this.meta = new FeedMeta() this.meta.title = seriesExpanded.name this.meta.description = seriesExpanded.description || '' this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png` + this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png` this.meta.feedUrl = feedUrl this.meta.link = `${serverAddress}/library/${libraryId}/series/${seriesExpanded.id}` this.meta.explicit = !!itemsWithTracks.some(li => li.media.metadata.explicit) // explicit if any item is explicit @@ -305,10 +316,12 @@ class Feed { this.entityUpdatedAt = seriesExpanded.updatedAt this.coverPath = firstItemWithCover?.coverPath || null + const coverFileExtension = this.coverPath ? Path.extname(media.coverPath) : null + this.meta.title = seriesExpanded.name this.meta.description = seriesExpanded.description || '' this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover` : `${this.serverAddress}/Logo.png` + this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png` this.meta.explicit = !!itemsWithTracks.some(li => li.media.metadata.explicit) // explicit if any item is explicit this.episodes = [] From e4a7e9d6b51cbafde5547bb5494272c32842df23 Mon Sep 17 00:00:00 2001 From: radekmuhlfeit2 <53485984+radekmuhlfeit2@users.noreply.github.com> Date: Wed, 1 Nov 2023 20:21:45 +0100 Subject: [PATCH 106/208] Create cs-CZ.json Czech strings. --- client/strings/cs-CZ.json | 729 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 729 insertions(+) create mode 100644 client/strings/cs-CZ.json diff --git a/client/strings/cs-CZ.json b/client/strings/cs-CZ.json new file mode 100644 index 000000000..06e019461 --- /dev/null +++ b/client/strings/cs-CZ.json @@ -0,0 +1,729 @@ +{ + "ButtonAdd": "Přidat", + "ButtonAddChapters": "Přidat kapitoly", + "ButtonAddDevice": "Přidat zařízení", + "ButtonAddLibrary": "Přidat knihovnu", + "ButtonAddPodcasts": "Přidat podcasty", + "ButtonAddUser": "Přidat uživatele", + "ButtonAddYourFirstLibrary": "Vytvořte svou první knihovnu", + "ButtonApply": "Aplikovat", + "ButtonApplyChapters": "Aplikovat kapitoly", + "ButtonAuthors": "Autoři", + "ButtonBrowseForFolder": "Vyhledat složku", + "ButtonCancel": "Zrušit", + "ButtonCancelEncode": "Zrušit kódování", + "ButtonChangeRootPassword": "Změnit 'Root' heslo", + "ButtonCheckAndDownloadNewEpisodes": "Zkontrolovat & stáhnout nové epizody", + "ButtonChooseAFolder": "Vybrat složku", + "ButtonChooseFiles": "Vybrat soubory", + "ButtonClearFilter": "Vymazat filtr", + "ButtonCloseFeed": "Zavřít podávání", + "ButtonCollections": "Sbírky", + "ButtonConfigureScanner": "Konfigurovat skener", + "ButtonCreate": "Vytvořit", + "ButtonCreateBackup": "Vytvořit zálohu", + "ButtonDelete": "Smazat", + "ButtonDownloadQueue": "Fronta", + "ButtonEdit": "Upravit", + "ButtonEditChapters": "Upravit kapitoly", + "ButtonEditPodcast": "Upravit podcast", + "ButtonForceReScan": "Vynutit opětovné skenování", + "ButtonFullPath": "Úplná cesta", + "ButtonHide": "Skrýt", + "ButtonHome": "Domů", + "ButtonIssues": "Problémy", + "ButtonLatest": "Nejnovější", + "ButtonLibrary": "Knihovna", + "ButtonLogout": "Odhlásit", + "ButtonLookup": "Vyhledat", + "ButtonManageTracks": "Správa tras", + "ButtonMapChapterTitles": "Mapovat názvy kapitol", + "ButtonMatchAllAuthors": "Shoda se všemi autory", + "ButtonMatchBooks": "Knihy zápalek", + "ButtonNevermind": "Nevadí", + "ButtonOk": "Ok", + "ButtonOpenFeed": "Otevřít kanál", + "ButtonOpenManager": "Otevřít správce", + "ButtonPlay": "Přehrát", + "ButtonPlaying": "Hraje", + "ButtonPlaylists": "Seznamy skladeb", + "ButtonPurgeAllCache": "Vymazat veškerou mezipaměť", + "ButtonPurgeItemsCache": "Vymazat mezipaměť položek", + "ButtonPurgeMediaProgress": "Vyčistit průběh médií", + "ButtonQueueAddItem": "Přidat do fronty", + "ButtonQueueRemoveItem": "Odstranit z fronty", + "ButtonQuickMatch": "Rychlá shoda", + "ButtonRead": "Číst", + "ButtonRemove": "Odstranit", + "ButtonRemoveAll": "Odstranit vše", + "ButtonRemoveAllLibraryItems": "Odstranit všechny položky knihovny", + "ButtonRemoveFromContinueListening": "Odstranit z pokračujícího poslechu", + "ButtonRemoveFromContinueReading": "Odstranit z pokračování ve čtení", + "ButtonRemoveSeriesFromContinueSeries": "Odstranit sérii z pokračování série", + "ButtonReScan": "Znovu skenovat", + "ButtonReset": "Resetovat", + "ButtonResetToDefault": "Obnovit výchozí", + "ButtonRestore": "Obnovit", + "ButtonSave": "Uložit", + "ButtonSaveAndClose": "Uložit a zavřít", + "ButtonSaveTracklist": "Uložit seznam skladeb", + "ButtonScan": "Skenovat", + "ButtonScanLibrary": "Knihovna skenů", + "ButtonSearch": "Hledat", + "ButtonSelectFolderPath": "Vybrat cestu ke složce", + "ButtonSeries": "Série", + "ButtonSetChaptersFromTracks": "Nastavit kapitoly ze stop", + "ButtonShiftTimes": "Časy posunu", + "ButtonShow": "Zobrazit", + "ButtonStartM4BEncode": "Spustit kódování M4B", + "ButtonStartMetadataEmbed": "Spustit vkládání metadat", + "ButtonSubmit": "Odeslat", + "ButtonTest": "Test", + "ButtonUpload": "Nahrát", + "ButtonUploadBackup": "Nahrát zálohu", + "ButtonUploadCover": "Nahrát obálku", + "ButtonUploadOPMLFile": "Nahrát soubor OPML", + "ButtonUserDelete": "Smazat uživatelský {0}", + "ButtonUserEdit": "Upravit uživatelské {0}", + "ButtonViewAll": "Zobrazit vše", + "ButtonYes": "Ano", + "HeaderAccount": "Účet", + "HeaderAdvanced": "Pokročilé", + "HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise", + "HeaderAudiobookTools": "Nástroje pro správu souborů audioknih", + "HeaderAudioTracks": "Zvukové stopy", + "HeaderBackups": "Zálohy", + "HeaderChangePassword": "Změnit heslo", + "HeaderChapters": "Kapitoly", + "HeaderChooseAFolder": "Vyberte složku", + "HeaderCollection": "Kolekce", + "HeaderCollectionItems": "Položky sbírky", + "HeaderCover": "Obálka", + "HeaderCurrentDownloads": "Aktuální stahování", + "HeaderDetails": "Podrobnosti", + "HeaderDownloadQueue": "Fronta stahování", + "HeaderEbookFiles": "Soubory elektronických knih", + "HeaderEmail": "E-mail", + "HeaderEmailSettings": "Nastavení e-mailu", + "HeaderEpisodes": "Epizody", + "HeaderEreaderDevices": "Čtečky elektronických knih", + "HeaderEreaderSettings": "Nastavení čtečky elektronických knih", + "HeaderFiles": "Soubory", + "HeaderFindChapters": "Najít kapitoly", + "HeaderIgnoredFiles": "Ignorované soubory", + "HeaderItemFiles": "Soubory položek", + "HeaderItemMetadataUtils": "Nástroje metadat položek", + "HeaderLastListeningSession": "Poslední poslechová relace", + "HeaderLatestEpisodes": "Poslední epizody", + "HeaderLibraries": "Knihovny", + "HeaderLibraryFiles": "Soubory knihovny", + "HeaderLibraryStats": "Statistiky knihovny", + "HeaderListeningSessions": "Poslechové relace", + "HeaderListeningStats": "Statistiky poslechu", + "HeaderLogin": "Přihlásit", + "HeaderLogs": "Záznamy", + "HeaderManageGenres": "Správa žánrů", + "HeaderManageTags": "Správa značek", + "HeaderMapDetails": "Detaily mapy", + "HeaderMatch": "Shoda", + "HeaderMetadataOrderOfPrecedence": "Pořadí priorit metadat", + "HeaderMetadataToEmbed": "Metadata pro vložení", + "HeaderNewAccount": "Nový účet", + "HeaderNewLibrary": "Nová knihovna", + "HeaderNotifications": "Oznámení", + "HeaderOpenRSSFeed": "Otevřít RSS kanál", + "HeaderOtherFiles": "Ostatní soubory", + "HeaderPermissions": "Oprávnění", + "HeaderPlayerQueue": "Fronta hráčů", + "HeaderPlaylist": "Seznam skladeb", + "HeaderPlaylistItems": "Položky playlistu", + "HeaderPodcastsToAdd": "Podcasty k přidání", + "HeaderPreviewCover": "Náhled obálky", + "HeaderRemoveEpisode": "Odstranit epizodu", + "HeaderRemoveEpisodes": "Odstranit {0} epizody", + "HeaderRSSFeedGeneral": "Podrobnosti o RSS", + "HeaderRSSFeedIsOpen": "Informační kanál RSS je otevřený", + "HeaderRSSFeeds": "RSS kanály", + "HeaderSavedMediaProgress": "Průběh uložených médií", + "HeaderSchedule": "Plán", + "HeaderScheduleLibraryScans": "Naplánovat automatické skenování knihoven", + "HeaderSession": "Session", + "HeaderSetBackupSchedule": "Nastavit plán zálohování", + "HeaderSettings": "Nastavení", + "HeaderSettingsDisplay": "Zobrazit", + "HeaderSettingsExperimental": "Experimentální funkce", + "HeaderSettingsGeneral": "Obecné", + "HeaderSettingsScanner": "Skener", + "HeaderSleepTimer": "Časovač vypnutí", + "HeaderStatsLargestItems": "Největší položky", + "HeaderStatsLongestItems": "Nejdelší položky (hod.)", + "HeaderStatsMinutesListeningChart": "Počet minut poslechu (posledních 7 dní)", + "HeaderStatsRecentSessions": "Poslední relace", + "HeaderStatsTop10Authors": "Top 10 autorů", + "HeaderStatsTop5Genres": "Top 5 žánrů", + "HeaderTableOfContents": "Obsah", + "HeaderTools": "Nástroje", + "HeaderUpdateAccount": "Aktualizovat účet", + "HeaderUpdateAuthor": "Aktualizovat autora", + "HeaderUpdateDetails": "Podrobnosti o aktualizaci", + "HeaderUpdateLibrary": "Aktualizovat knihovnu", + "HeaderUsers": "Uživatelé", + "HeaderYourStats": "Vaše statistiky", + "LabelAbridged": "Zkráceno", + "LabelAccountType": "Typ účtu", + "LabelAccountTypeAdmin": "Správce", + "LabelAccountTypeGuest": "Host", + "LabelAccountTypeUser": "Uživatel", + "LabelActivity": "Aktivita", + "LabelAdded": "Přidáno", + "LabelAddedAt": "Přidáno v", + "LabelAddToCollection": "Přidat do kolekce", + "LabelAddToCollectionBatch": "Přidat {0} knihy do kolekce", + "LabelAddToPlaylist": "Přidat do playlistu", + "LabelAddToPlaylistBatch": "Přidat {0} položky do playlistu", + "LabelAdminUsersOnly": "Pouze administrátoři", + "LabelAll": "Vše", + "LabelAllUsers": "Všichni uživatelé", + "LabelAllUsersExcludingGuests": "Všichni uživatelé kromě hostů", + "LabelAllUsersIncludingGuests": "Všichni uživatelé včetně hostů", + "LabelAlreadyInYourLibrary": "Již ve vaší knihovně", + "LabelAppend": "Připojit", + "LabelAuthor": "Autor", + "LabelAuthorFirstLast": "Autor (první poslední)", + "LabelAuthorLastFirst": "Autor (poslední, první)", + "LabelAuthors": "Autoři", + "LabelAutoDownloadEpisodes": "Automatické stahování epizod", + "LabelBackToUser": "Zpět k uživateli", + "LabelBackupLocation": "Umístění zálohy", + "LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování", + "LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups", + "LabelBackupsMaxBackupSize": "Maximální velikost zálohy (v GB)", + "LabelBackupsMaxBackupSizeHelp": "Jako pojistka proti chybné konfiguraci se zálohy nezdaří, pokud překročí nastavenou velikost.", + "LabelBackupsNumberToKeep": "Počet záloh, které se mají uchovat", + "LabelBackupsNumberToKeepHelp": "Najednou bude odstraněna pouze 1 záloha, takže pokud již máte více záloh, měli byste je odstranit ručně.", + "LabelBitrate": "Datový tok", + "LabelBooks": "Knihy", + "LabelChangePassword": "Změnit heslo", + "LabelChannels": "Kanály", + "LabelChapters": "Kapitoly", + "LabelChaptersFound": "Kapitoly nalezeny", + "LabelChapterTitle": "Název kapitoly", + "LabelClickForMoreInfo": "Klikněte pro více informací", + "LabelClosePlayer": "Zavřít přehrávač", + "LabelCodec": "Kodek", + "LabelCollapseSeries": "Sbalit sérii", + "LabelCollection": "Sbírka", + "LabelCollections": "Sbírky", + "LabelComplete": "Dokončeno", + "LabelConfirmPassword": "Potvrdit heslo", + "LabelContinueListening": "Pokračovat v poslechu", + "LabelContinueReading": "Pokračovat ve čtení", + "LabelContinueSeries": "Pokračovat v řadě", + "LabelCover": "Obálka", + "LabelCoverImageURL": "URL obálkového obrázku", + "LabelCreatedAt": "Vytvořeno v", + "LabelCronExpression": "Cron výraz", + "LabelCurrent": "Aktuální", + "LabelCurrently": "Aktuálně:", + "LabelCustomCronExpression": "Vlastní cron výraz:", + "LabelDatetime": "Datum a čas", + "LabelDeleteFromFileSystemCheckbox": "Smazat ze souborového systému (zrušte zaškrtnutí pro odstranění pouze z databáze)", + "LabelDescription": "Popis", + "LabelDeselectAll": "Odznačit vše", + "LabelDevice": "Zařízení", + "LabelDeviceInfo": "Informace o zařízení", + "LabelDeviceIsAvailableTo": "Zařízení je dostupné pro...", + "LabelDirectory": "Adresář", + "LabelDiscFromFilename": "Disk z názvu souboru", + "LabelDiscFromMetadata": "Disk z metadat", + "LabelDiscover": "Objevit", + "LabelDownload": "Stáhnout", + "LabelDownloadNEpisodes": "Stáhnout {0} epizody", + "LabelDuration": "Doba trvání", + "LabelDurationFound": "Doba trvání nalezena:", + "LabelEbook": "Elektronická kniha", + "LabelEbooks": "E-knihy", + "LabelEdit": "Upravit", + "LabelEmail": "E-mail", + "LabelEmailSettingsFromAddress": "Z adresy", + "LabelEmailSettingsSecure": "Zabezpečené", + "LabelEmailSettingsSecureHelp": "Pokud je true, připojení bude při připojování k serveru používat TLS. Pokud je false, použije se protokol TLS, pokud server podporuje rozšíření STARTTLS. Ve většině případů nastavte tuto hodnotu na true, pokud se připojujete k portu 465. Pro port 587 nebo 25 ponechte hodnotu false. (z nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Testovací adresa", + "LabelEmbeddedCover": "Vložený obal", + "LabelEnable": "Povolit", + "LabelEnd": "Konec", + "LabelEpisode": "Epizoda", + "LabelEpisodeTitle": "Název epizody", + "LabelEpisodeType": "Typ epizody", + "LabelExample": "Příklad", + "LabelExplicit": "Explicitní", + "LabelFeedURL": "URL zdroje", + "LabelFile": "Soubor", + "LabelFileBirthtime": "Čas narození souboru", + "LabelFileModified": "Soubor změněn", + "LabelFilename": "Název souboru", + "LabelFilterByUser": "Filtrovat podle uživatele", + "LabelFindEpisodes": "Najít epizody", + "LabelFinished": "Dokončeno", + "LabelFolder": "Složka", + "LabelFolders": "Složky", + "LabelFontFamily": "Rodina písem", + "LabelFontScale": "Měřítko písma", + "LabelFormat": "Formát", + "LabelGenre": "Žánr", + "LabelGenres": "Žánry", + "LabelHardDeleteFile": "Trvale smazat soubor", + "LabelHasEbook": "Obsahuje elektronickou knihu", + "LabelHasSupplementaryEbook": "Obsahuje doplňkovou e-knihu", + "LabelHost": "Hostitel", + "LabelHour": "Hodina", + "LabelIcon": "Ikona", + "LabelImageURLFromTheWeb": "URL obrázku z webu", + "LabelIncludeInTracklist": "Zahrnout do seznamu skladeb", + "LabelIncomplete": "Neúplné", + "LabelInProgress": "Probíhá", + "LabelInterval": "Interval", + "LabelIntervalCustomDailyWeekly": "Vlastní denně/týdně", + "LabelIntervalEvery12Hours": "Každých 12 hodin", + "LabelIntervalEvery15Minutes": "Každých 15 minut", + "LabelIntervalEvery2Hours": "Každé 2 hodiny", + "LabelIntervalEvery30Minutes": "Každých 30 minut", + "LabelIntervalEvery6Hours": "Každých 6 hodin", + "LabelIntervalEveryDay": "Každý den", + "LabelIntervalEveryHour": "Každou hodinu", + "LabelInvalidParts": "Neplatné části", + "LabelInvert": "Invertovat", + "LabelItem": "Položka", + "LabelLanguage": "Jazyk", + "LabelLanguageDefaultServer": "Výchozí jazyk serveru", + "LabelLastBookAdded": "Přidán poslední kniha", + "LabelLastBookUpdated": "Poslední kniha aktualizována", + "LabelLastSeen": "Naposledy viděno", + "LabelLastTime": "Naposledy", + "LabelLastUpdate": "Poslední aktualizace", + "LabelLayout": "Rozvržení", + "LabelLayoutSinglePage": "Jedna stránka", + "LabelLayoutSplitPage": "Rozdělit stránku", + "LabelLess": "Méně", + "LabelLibrariesAccessibleToUser": "Knihovny přístupné uživateli", + "LabelLibrary": "Knihovna", + "LabelLibraryItem": "Položka knihovny", + "LabelLibraryName": "Název knihovny", + "LabelLimit": "Limit", + "LabelLineSpacing": "Řádkování", + "LabelListenAgain": "Poslouchat znovu", + "LabelLogLevelDebug": "Ladit", + "LabelLogLevelInfo": "Informace", + "LabelLogLevelWarn": "Varovat", + "LabelLookForNewEpisodesAfterDate": "Hledat nové epizody po tomto datu", + "LabelMediaPlayer": "Přehrávač médií", + "LabelMediaType": "Typ média", + "LabelMetadataOrderOfPrecedenceDescription": "1 je nejnižší priorita, 5 je nejvyšší priorita", + "LabelMetadataProvider": "Poskytovatel metadat", + "LabelMetaTag": "Meta tag", + "LabelMetaTags": "Meta tagy", + "LabelMinute": "Minuta", + "LabelMissing": "Chybí", + "LabelMissingParts": "Chybějící díly", + "LabelMore": "Více", + "LabelMoreInfo": "Více informací", + "LabelName": "Jméno", + "LabelNarrator": "Předčítání", + "LabelNarrators": "Předčítání", + "LabelNew": "Nový", + "LabelNewestAuthors": "Nejnovější autoři", + "LabelNewestEpisodes": "Nejnovější epizody", + "LabelNewPassword": "Nové heslo", + "LabelNextBackupDate": "Datum další zálohy", + "LabelNextScheduledRun": "Další naplánované spuštění", + "LabelNoEpisodesSelected": "Nebyly vybrány žádné epizody", + "LabelNotes": "Poznámky", + "LabelNotFinished": "Nedokončeno", + "LabelNotificationAppriseURL": "URL adresy Apprise", + "LabelNotificationAvailableVariables": "Dostupné proměnné", + "LabelNotificationBodyTemplate": "Šablona těla", + "LabelNotificationEvent": "Událost oznámení", + "LabelNotificationsMaxFailedAttempts": "Maximální počet neúspěšných pokusů", + "LabelNotificationsMaxFailedAttemptsHelp": "Oznámení jsou vypnuta, pokud se jim to nepodaří odeslat", + "LabelNotificationsMaxQueueSize": "Maximální velikost fronty pro oznamovací události", + "LabelNotificationsMaxQueueSizeHelp": "Události jsou omezeny na 1 za sekundu. Události budou ignorovány, pokud je fronta v maximální velikosti. Tím se zabrání spamování oznámení.", + "LabelNotificationTitleTemplate": "Šablona názvu", + "LabelNotStarted": "Nespuštěno", + "LabelNumberOfBooks": "Počet knih", + "LabelNumberOfEpisodes": "# epizod", + "LabelOpenRSSFeed": "Otevřít RSS kanál", + "LabelOverwrite": "Přepsat", + "LabelPassword": "Heslo", + "LabelPath": "Cesta", + "LabelPermissionsAccessAllLibraries": "Má přístup ke všem knihovnám", + "LabelPermissionsAccessAllTags": "Má přístup ke všem značkám", + "LabelPermissionsAccessExplicitContent": "Má přístup k explicitnímu obsahu", + "LabelPermissionsDelete": "Může mazat", + "LabelPermissionsDownload": "Lze stáhnout", + "LabelPermissionsUpdate": "Může aktualizovat", + "LabelPermissionsUpload": "Lze nahrávat", + "LabelPhotoPathURL": "Cesta k fotografii/URL", + "LabelPlaylists": "Seznamy skladeb", + "LabelPlayMethod": "Metoda přehrávání", + "LabelPodcast": "Podcast", + "LabelPodcasts": "Podcasty", + "LabelPodcastType": "Typ podcastu", + "LabelPort": "Port", + "LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)", + "LabelPreventIndexing": "Zabraňte indexování vašeho zdroje adresáři podcastů iTunes a Google", + "LabelPrimaryEbook": "Hlavní e-kniha", + "LabelProgress": "Průběh", + "LabelProvider": "Poskytovatel", + "LabelPubDate": "Datum hospody", + "LabelPublisher": "Vydavatel", + "LabelPublishYear": "Rok publikování", + "LabelRead": "Číst", + "LabelReadAgain": "Číst znovu", + "LabelReadEbookWithoutProgress": "Číst e-knihu bez zachování průběhu", + "LabelRecentlyAdded": "Nedávno přidáno", + "LabelRecentSeries": "Poslední série", + "LabelRecommended": "Doporučeno", + "LabelRegion": "Region", + "LabelReleaseDate": "Datum vydání", + "LabelRemoveCover": "Sejmout kryt", + "LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka", + "LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka", + "LabelRSSFeedOpen": "Otevření RSS kanálu", + "LabelRSSFeedPreventIndexing": "Zabránit indexování", + "LabelRSSFeedSlug": "RSS kanál Slug", + "LabelRSSFeedURL": "URL RSS kanálu", + "LabelSearchTerm": "Hledaný výraz", + "LabelSearchTitle": "Název vyhledávání", + "LabelSearchTitleOrASIN": "Vyhledat název nebo ASIN", + "LabelSeason": "Sezóna", + "LabelSelectAllEpisodes": "Vybrat všechny epizody", + "LabelSelectEpisodesShowing": "Vyberte {0} epizody, které se zobrazují", + "LabelSelectUsers": "Vybrat uživatele", + "LabelSendEbookToDevice": "Odeslat e-knihu do...", + "LabelSequence": "Sekvence", + "LabelSeries": "Série", + "LabelSeriesName": "Název série", + "LabelSeriesProgress": "Průběh série", + "LabelSetEbookAsPrimary": "Nastavit jako primární", + "LabelSetEbookAsSupplementary": "Nastavit jako doplňkové", + "LabelSettingsAudiobooksOnly": "Pouze audioknihy", + "LabelSettingsAudiobooksOnlyHelp": "Povolením tohoto nastavení budou soubory e-knih ignorovány, pokud nejsou ve složce audioknih, v takovém případě budou nastaveny jako doplňkové e-knihy", + "LabelSettingsBookshelfViewHelp": "Skeumorfní design s dřevěnými policemi", + "LabelSettingsChromecastSupport": "Podpora Chromecastu", + "LabelSettingsDateFormat": "Formát data", + "LabelSettingsDisableWatcher": "Zakázat sledování", + "LabelSettingsDisableWatcherForLibrary": "Zakázat sledování složky pro knihovnu", + "LabelSettingsDisableWatcherHelp": "Zakáže automatické přidávání/aktualizaci položek při zjištění změn v souboru. *Vyžaduje restart serveru", + "LabelSettingsEnableWatcher": "Povolit sledování", + "LabelSettingsEnableWatcherForLibrary": "Povolit sledování složky pro knihovnu", + "LabelSettingsEnableWatcherHelp": "Umožňuje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru", + "LabelSettingsExperimentalFeatures": "Experimentální funkce", + "LabelSettingsExperimentalFeaturesHelp": "Vyvíjené funkce, které by mohly využít vaši zpětnou vazbu a pomoc s testováním. Kliknutím otevřete diskuzi na githubu.", + "LabelSettingsFindCovers": "Najít obálky", + "LabelSettingsFindCoversHelp": "Pokud vaše audiokniha nemá vloženou obálku nebo obrázek obálky uvnitř složky, skener se pokusí obálku najít.<br>Poznámka: Tím se prodlouží doba skenování", + "LabelSettingsHideSingleBookSeries": "Skrýt sérii jednotlivých knih", + "LabelSettingsHideSingleBookSeriesHelp": "Série, které mají jednu knihu, budou skryty na stránce série a na domovské stránce.", + "LabelSettingsHomePageBookshelfView": "Domovská stránka používá regálové zobrazení", + "LabelSettingsLibraryBookshelfView": "Knihovna používá regálové zobrazení", + "LabelSettingsParseSubtitles": "Analyzovat titulky", +"LabelSettingsParseSubtitlesHelp": "Extrahovat titulky z názvů složek audioknih.<br>Titulky musí být odděleny znakem \" - \"<br>tj. \"Název knihy - Zde titulky\" má podtitul \"Zde titulky\"", + "LabelSettingsPreferMatchedMetadata": "Preferovat odpovídající metadata", + "LabelSettingsPreferMatchedMetadataHelp": "Shodná data přepíší detaily položky při použití Rychlého výběru. Ve výchozím nastavení funkce Quick Match vyplní pouze chybějící podrobnosti.", + "LabelSettingsSkipMatchingBooksWithASIN": "Přeskočit odpovídající knihy, které již mají ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Přeskočit odpovídající knihy, které již mají ISBN", + "LabelSettingsSortingIgnorePrefixes": "Ignorovat prefixy při třídění", + "LabelSettingsSortingIgnorePrefixesHelp": "tj. pro předponu \"the\" název knihy \"Název knihy\" by se třídil jako \"Název knihy, The\"", + "LabelSettingsSquareBookCovers": "Použít čtvercové obálky knih", + "LabelSettingsSquareBookCoversHelp": "Preferovat použití čtvercových desek před standardními obálkami 1.6:1", + "LabelSettingsStoreCoversWithItem": "Uložit obaly s položkou", + "LabelSettingsStoreCoversWithItemHelp": "Ve výchozím nastavení jsou obálky uloženy v adresáři /metadata/items, povolením tohoto nastavení se obálky uloží do složky položek knihovny. Zůstane zachován pouze jeden soubor s názvem \"cover\"", + "LabelSettingsStoreMetadataWithItem": "Uložit metadata s položkou", + "LabelSettingsStoreMetadataWithItemHelp": "Ve výchozím nastavení jsou soubory metadat uloženy v adresáři /metadata/items, povolením tohoto nastavení budou soubory metadat uloženy ve složkách položek knihovny", + "LabelSettingsTimeFormat": "Formát času", + "LabelShowAll": "Zobrazit vše", + "LabelSize": "Velikost", + "LabelSleepTimer": "Časovač vypnutí", + "LabelSlug": "Slimák", + "LabelStart": "Spustit", + "LabelStarted": "Spuštěno", + "LabelStartedAt": "Spuštěno v", + "LabelStartTime": "Čas zahájení", + "LabelStatsAudioTracks": "Zvukové stopy", + "LabelStatsAuthors": "Autoři", + "LabelStatsBestDay": "Nejlepší den", + "LabelStatsDailyAverage": "Denní průměr", + "LabelStatsDays": "Dny", + "LabelStatsDaysListened": "Dny odposloucháváno", + "LabelStatsHours": "Hodiny", + "LabelStatsInARow": "v řadě", + "LabelStatsItemsFinished": "Položky dokončeny", + "LabelStatsItemsInLibrary": "Položky v knihovně", + "LabelStatsMinutes": "minut", + "LabelStatsMinutesListening": "Minuty poslechu", + "LabelStatsOverallDays": "Celkový počet dní", + "LabelStatsOverallHours": "Celkové hodiny", + "LabelStatsWeekListening": "Týdenní poslech", + "LabelSubtitle": "Titulky", + "LabelSupportedFileTypes": "Podporované typy souborů", + "LabelTag": "Značka", + "LabelTags": "Značky", + "LabelTagsAccessibleToUser": "Tagy přístupné uživateli", + "LabelTagsNotAccessibleToUser": "Značky nejsou přístupné uživateli", + "LabelTasks": "Úlohy běží", + "LabelTheme": "Téma", + "LabelThemeDark": "Tmavý", + "LabelThemeLight": "Světlo", + "LabelTimeBase": "Časová základna", + "LabelTimeListened": "Poslouchaný čas", + "LabelTimeListenedToday": "Čas poslouchaný dnes", + "LabelTimeRemaining": "{0} zbývající", + "LabelTimeToShift": "Čas posunu v sekundách", + "LabelTitle": "Název", + "LabelToolsEmbedMetadata": "Vložit metadata", + "LabelToolsEmbedMetadataDescription": "Vložit metadata do zvukových souborů včetně titulního obrázku a kapitoly.", + "LabelToolsMakeM4b": "Vytvořit soubor audioknihy M4B", + "LabelToolsMakeM4bDescription": "Vygenerovat soubor . Soubor audioknihy M4B s vloženými metadaty, titulním obrázkem a Kapitoly.", + "LabelToolsSplitM4b": "Rozdělit M4B na MP3", + "LabelToolsSplitM4bDescription": "Vytvořit MP3 z M4B splitu od Kapitoly s vloženými metadaty, obrázkem obálky a Kapitoly.", + "LabelTotalDuration": "Celková doba trvání", + "LabelTotalTimeListened": "Celkový poslouchaný čas", + "LabelTrackFromFilename": "Stopa z názvu souboru", + "LabelTrackFromMetadata": "Sledovat z metadat", + "LabelTracks": "Stopy", + "LabelTracksMultiTrack": "Více stop", + "LabelTracksNone": "Žádné stopy", + "LabelTracksSingleTrack": "Jedna stopa", + "LabelType": "Typ", + "LabelUnabridged": "Nezkráceno", + "LabelUnknown": "Neznámý", + "LabelUpdateCover": "Aktualizovat obálku", + "LabelUpdateCoverHelp": "Povolit přepsání existujících obálek pro vybrané knihy, pokud je nalezena shoda", + "LabelUpdatedAt": "Aktualizováno v", + "LabelUpdateDetails": "Podrobnosti o aktualizaci", + "LabelUpdateDetailsHelp": "Povolit přepsání existujících údajů o vybraných knihách, když je nalezena shoda", + "LabelUploaderDragAndDrop": "Přetáhnout soubory nebo složky", + "LabelUploaderDropFiles": "Odstranit soubory", + "LabelUseChapterTrack": "Použít stopu kapitol", + "LabelUseFullTrack": "Použít celou trasu", + "LabelUser": "Uživatel", + "LabelUsername": "Uživatelské jméno", + "LabelValue": "Hodnota", + "LabelVersion": "Verze", + "LabelViewBookmarks": "Zobrazit záložky", + "LabelViewChapters": "Zobrazit kapitoly", + "LabelViewQueue": "Zobrazit frontu hráčů", + "LabelVolume": "Svazek", + "LabelWeekdaysToRun": "Dny v týdnu ke spuštění", + "LabelYourAudiobookDuration": "Doba trvání vaší audioknihy", + "LabelYourBookmarks": "Vaše záložky", + "LabelYourPlaylists": "Vaše playlisty", + "LabelYourProgress": "Váš pokrok", + "MessageAddToPlayerQueue": "Přidat do fronty hráčů", + "MessageAppriseDescription": "Abyste mohli používat tuto funkci, musíte mít spuštěnou instanci <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nebo API, které bude zpracovávat stejné požadavky. <br />Adresa URL API Apprise by měla být úplná URL cesta pro odeslání oznámení, např. pokud je vaše instance API obsluhována na adrese <code>http://192.168.1.1:8337</code> pak byste měli zadat <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "Zálohy zahrnují uživatele, průběh uživatele, podrobnosti o položkách knihovny, nastavení serveru a obrázky uložené v <code>/metadata/items</code> a <code>/metadata/authors</code>. Zálohy <strong>ne</strong> zahrnují všechny soubory uložené ve složkách knihovny.", + "MessageBatchQuickMatchDescription": "Rychlá shoda se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlá shoda přepsat stávající obálky a/nebo metadata.", + "MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku", + "MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály", + "MessageBookshelfNoSeries": "Nemáte žádnou sérii", + "MessageChapterEndIsAfter": "Konec kapitoly je po skončení audioknihy", + "MessageChapterErrorFirstNotZero": "První kapitola musí začínat na 0", + "MessageChapterErrorStartGteDuration": "Neplatný čas začátku musí být kratší než doba trvání audioknihy", + "MessageChapterErrorStartLtPrev": "Neplatný čas začátku musí být větší nebo roven času začátku předchozí kapitoly", + "MessageChapterStartIsAfter": "Začátek kapitoly je po skončení audioknihy", + "MessageCheckingCron": "Kontrola cronu...", + "MessageConfirmCloseFeed": "Opravdu chcete zavřít tento kanál?", + "MessageConfirmDeleteBackup": "Opravdu chcete smazat zálohu pro {0}?", + "MessageConfirmDeleteFile": "Tím dojde ke smazání souboru ze souborového systému. Jsi si jistý?", + "MessageConfirmDeleteLibrary": "Opravdu chcete trvale smazat knihovnu \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "Tím se odstraní položka knihovny z databáze a souborového systému. Jsi si jistý?", + "MessageConfirmDeleteLibraryItems": "Toto smaže {0} položky knihovny z databáze a vašeho souborového systému. Jsi si jistý?", + "MessageConfirmDeleteSession": "Opravdu chcete smazat tuto relaci?", + "MessageConfirmForceReScan": "Opravdu chcete vynutit opětovnou kontrolu?", + "MessageConfirmMarkAllEpisodesFinished": "Opravdu chcete označit všechny epizody jako dokončené?", + "MessageConfirmMarkAllEpisodesNotFinished": "Opravdu chcete označit všechny epizody jako nedokončené?", + "MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?", + "MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?", + "MessageConfirmQuickEmbed": "Varování! Rychlé vložení nezálohuje vaše zvukové soubory. Ujistěte se, že máte zálohu zvukových souborů. <br><br>Chcete pokračovat?", + "MessageConfirmRemoveAllChapters": "Opravdu chcete odstranit všechny kapitoly?", + "MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?", + "MessageConfirmRemoveCollection": "Opravdu chcete odstranit kolekci \"{0}\"?", + "MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?", + "MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?", + "MessageConfirmRemoveNarrator": "Opravdu chcete odebrat předčítání \"{0}\"?", + "MessageConfirmRemovePlaylist": "Opravdu chcete odstranit svůj playlist \"{0}\"?", + "MessageConfirmRenameGenre": "Opravdu chcete přejmenovat žánr \"{0}\" na \"{1}\" pro všechny položky?", + "MessageConfirmRenameGenreMergeNote": "Poznámka: Tento žánr již existuje, takže budou sloučeny.", + "MessageConfirmRenameGenreWarning": "Varování! Podobný žánr s jiným obalem již existuje \"{0}\".", + "MessageConfirmRenameTag": "Opravdu chcete přejmenovat tag \"{0}\" na \"{1}\" pro všechny položky?", + "MessageConfirmRenameTagMergeNote": "Poznámka: Tato značka již existuje, takže budou sloučeny.", + "MessageConfirmRenameTagWarning": "Varování! Podobná značka s jinými velkými a malými písmeny již existuje \"{0}\".", + "MessageConfirmReScanLibraryItems": "Opravdu chcete znovu zkontrolovat {0} položky?", + "MessageConfirmSendEbookToDevice": "Opravdu chcete odeslat e-knihu {0} {1}\" do zařízení \"{2}\"?", + "MessageDownloadingEpisode": "Stahuji epizodu", + "MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop", + "MessageEmbedFinished": "Vložení dokončeno!", + "MessageEpisodesQueuedForDownload": "{0} epizody zařazené do fronty ke stažení", + "MessageFeedURLWillBe": "URL zdroje bude {0}", + "MessageFetching": "Načítání...", + "MessageForceReScanDescription": "znovu otestuje všechny soubory jako novou kontrolu. Zvuk file ID3 tagy, OPF soubory a textové soubory budou naskenovány jako nové.", + "MessageImportantNotice": "Důležité upozornění!", + "MessageInsertChapterBelow": "Vložit kapitolu níže", + "MessageItemsSelected": "{0} vybraných položek", + "MessageItemsUpdated": "{0} položky byly aktualizovány", + "MessageJoinUsOn": "Přidejte se k nám", + "MessageListeningSessionsInTheLastYear": "{0} poslechových relací za poslední rok", + "MessageLoading": "Načítá se...", + "MessageLoadingFolders": "Načítám složky...", + "MessageM4BFailed": "M4B se nezdařilo!", + "MessageM4BFinished": "M4B dokončen!", + "MessageMapChapterTitles": "Mapování názvů kapitol na stávající audioknihu Kapitoly bez úpravy časových razítek", + "MessageMarkAllEpisodesFinished": "Označit všechny epizody za ukončené", + "MessageMarkAllEpisodesNotFinished": "Označit všechny epizody jako nedokončené", + "MessageMarkAsFinished": "Označit jako dokončené", + "MessageMarkAsNotFinished": "Označit jako nedokončené", + "MessageMatchBooksDescription": "pokusí se spárovat knihy v knihovně s knihou od vybraného vyhledávače a vyplnit prázdné údaje a obálku. Nepřepisuje detaily.", + "MessageNoAudioTracks": "Žádné zvukové stopy", + "MessageNoAuthors": "Žádní autoři", + "MessageNoBackups": "Žádné zálohy", + "MessageNoBookmarks": "Žádné záložky", + "MessageNoChapters": "Žádné kapitoly", + "MessageNoCollections": "Žádné sbírky", + "MessageNoCoversFound": "Nebyly nalezeny žádné obálky", + "MessageNoDescription": "Bez popisu", + "MessageNoDownloadsInProgress": "Momentálně neprobíhá žádné stahování", + "MessageNoDownloadsQueued": "Žádné stahování ve frontě", + "MessageNoEpisodeMatchesFound": "Nebyly nalezeny žádné odpovídající epizody", + "MessageNoEpisodes": "Žádné epizody", + "MessageNoFoldersAvailable": "Nejsou k dispozici žádné složky", + "MessageNoGenres": "Žádné žánry", + "MessageNoIssues": "Žádné problémy", + "MessageNoItems": "Žádné položky", + "MessageNoItemsFound": "Nebyly nalezeny žádné položky", + "MessageNoListeningSessions": "Žádné poslechové relace", + "MessageNoLogs": "Žádné protokoly", + "MessageNoMediaProgress": "Žádný mediální průběh", + "MessageNoNotifications": "Žádná oznámení", + "MessageNoPodcastsFound": "Nebyly nalezeny žádné podcasty", + "MessageNoResults": "Žádné výsledky", + "MessageNoSearchResultsFor": "Nebyly nalezeny žádné výsledky hledání pro \"{0}\"", + "MessageNoSeries": "Žádné řady", + "MessageNoTags": "Žádné tagy", + "MessageNoTasksRunning": "Nejsou spuštěny žádné úlohy", + "MessageNotYetImplemented": "Ještě není implementováno", + "MessageNoUpdateNecessary": "Není nutná žádná aktualizace", + "MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace", + "MessageNoUserPlaylists": "Nemáte žádné playlisty", + "MessageOr": "nebo", + "MessagePauseChapter": "Pozastavit přehrávání kapitoly", + "MessagePlayChapter": "Poslechnout si začátek kapitoly", + "MessagePlaylistCreateFromCollection": "Vytvořit seznam skladeb ze sbírky", + "MessagePodcastHasNoRSSFeedForMatching": "Podcast nemá žádnou adresu URL kanálu RSS, kterou by mohl použít pro porovnávání", + "MessageQuickMatchDescription": "Vyplňte prázdné detaily položky a obálku prvním výsledkem shody z '{0}'. Nepřepisuje podrobnosti, pokud není povoleno nastavení serveru \"Preferovat odpovídající metadata\".", + "MessageRemoveChapter": "Odstranit kapitolu", + "MessageRemoveEpisodes": "Odstranit {0} epizodu", + "MessageRemoveFromPlayerQueue": "Odstranit z fronty hráčů", + "MessageRemoveUserWarning": "Opravdu chcete trvale smazat uživatele \"{0}\"?", + "MessageReportBugsAndContribute": "Hlásit chyby, žádat o funkce a přispívat", + "MessageResetChaptersConfirm": "Opravdu chcete resetovat kapitoly a vrátit zpět provedené změny?", + "MessageRestoreBackupConfirm": "Opravdu chcete obnovit zálohu vytvořenou dne?", + "MessageRestoreBackupWarning": "Obnovení zálohy přepíše celou databázi umístěnou v /config a obálku obrázků v /metadata/items & /metadata/authors.<br /><br />Backups nezmění žádné soubory ve složkách knihovny. Pokud jste povolili nastavení serveru pro ukládání obrázků obalu a metadat do složek knihovny, nebudou zálohovány ani přepsány.<br /><br />Všichni klienti používající váš server budou automaticky obnoveni.", + "MessageSearchResultsFor": "Výsledky hledání pro", + "MessageServerCouldNotBeReached": "Server nebyl dostupný", + "MessageSetChaptersFromTracksDescription": "Nastavit kapitoly jako kapitolu a název kapitoly jako název zvukového souboru", + "MessageStartPlaybackAtTime": "Spustit přehrávání pro \"{0}\" v {1}?", + "MessageThinking": "Přemýšlení...", + "MessageUploaderItemFailed": "Nahrávání se nezdařilo", + "MessageUploaderItemSuccess": "Nahráno bylo úspěšně!", + "MessageUploading": "Odesílám...", + "MessageValidCronExpression": "Platný cron výraz", + "MessageWatcherIsDisabledGlobally": "Watcher je globálně zakázán v nastavení serveru", + "MessageXLibraryIsEmpty": "{0} knihovna je prázdná!", + "MessageYourAudiobookDurationIsLonger": "Doba trvání audioknihy je delší než nalezená délka", + "MessageYourAudiobookDurationIsShorter": "Délka audioknihy je kratší, než byla nalezena." + "NoteChangeRootPassword": "Uživatel root je jediný uživatel, který může mít prázdné heslo", + "NoteChapterEditorTimes": "Poznámka: Čas začátku první kapitoly musí zůstat v 0:00 a čas začátku poslední kapitoly nesmí překročit tuto dobu trvání audioknihy.", + "NoteFolderPicker": "Poznámka: složky, které jsou již namapovány, nebudou zobrazeny", + "NoteFolderPickerDebian": "Poznámka: Výběr složek pro instalaci debianu není plně implementován. Cestu ke své knihovně byste měli zadat přímo.", + "NoteRSSFeedPodcastAppsHttps": "Upozornění: Většina aplikací pro podcasty bude vyžadovat, aby adresa URL kanálu RSS používala protokol HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "Upozornění: 1 nebo více epizod nemá datum vydání. Některé podcastové aplikace to vyžadují.", + "NoteUploaderFoldersWithMediaFiles": "Se složkami s multimediálními soubory bude zacházeno jako se samostatnými položkami knihovny.", + "NoteUploaderOnlyAudioFiles": "Pokud nahráváte pouze zvukové soubory, bude s každým zvukovým souborem zacházeno jako se samostatnou audioknihou.", + "NoteUploaderUnsupportedFiles": "Nepodporované soubory jsou ignorovány. Při výběru nebo přetažení složky jsou ostatní soubory, které nejsou ve složce položek, ignorovány.", + "PlaceholderNewCollection": "Nový název kolekce", + "PlaceholderNewFolderPath": "Nová cesta ke složce", + "PlaceholderNewPlaylist": "Nový název playlistu", + "PlaceholderSearch": "Hledat..", + "PlaceholderSearchEpisode": "Hledat epizodu..", + "ToastAccountUpdateFailed": "Aktualizace účtu se nezdařila", + "ToastAccountUpdateSuccess": "Účet aktualizován", + "ToastAuthorImageRemoveFailed": "Nepodařilo se odstranit obrázek", + "ToastAuthorImageRemoveSuccess": "Obrázek autora odstraněn", + "ToastAuthorUpdateFailed": "Aktualizace autora se nezdařila", + "ToastAuthorUpdateMerged": "Autor sloučen", + "ToastAuthorUpdateSuccess": "Autor aktualizován", + "ToastAuthorUpdateSuccessNoImageFound": "Autor aktualizován (nebyl nalezen žádný obrázek)", + "ToastBackupCreateFailed": "Vytvoření zálohy se nezdařilo", + "ToastBackupCreateSuccess": "Záloha vytvořena", + "ToastBackupDeleteFailed": "Nepodařilo se smazat zálohu", + "ToastBackupDeleteSuccess": "Záloha smazána", + "ToastBackupRestoreFailed": "Nepodařilo se obnovit zálohu", + "ToastBackupUploadFailed": "Nepodařilo se nahrát zálohu", + "ToastBackupUploadSuccess": "Záloha nahrána", + "ToastBatchUpdateFailed": "Dávková aktualizace se nezdařila", + "ToastBatchUpdateSuccess": "Hromadná aktualizace proběhla úspěšně", + "ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo", + "ToastBookmarkCreateSuccess": "Přidána záložka", + "ToastBookmarkRemoveFailed": "Nepodařilo se odstranit záložku", + "ToastBookmarkRemoveSuccess": "Záložka odstraněna", + "ToastBookmarkUpdateFailed": "Aktualizace záložky se nezdařila", + "ToastBookmarkUpdateSuccess": "Záložka aktualizována", + "ToastChaptersHaveErrors": "Kapitoly obsahují chyby", + "ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy", + "ToastCollectionItemsRemoveFailed": "Nepodařilo se odstranit položky z kolekce", + "ToastCollectionItemsRemoveSuccess": "Položky odstraněny z kolekce", + "ToastCollectionRemoveFailed": "Nepodařilo se odstranit kolekci", + "ToastCollectionRemoveSuccess": "Kolekce odstraněna", + "ToastCollectionUpdateFailed": "Aktualizace kolekce se nezdařila", + "ToastCollectionUpdateSuccess": "Kolekce aktualizována", + "ToastItemCoverUpdateFailed": "Aktualizace obalu položky se nezdařila", + "ToastItemCoverUpdateSuccess": "Obal předmětu byl aktualizován", + "ToastItemDetailsUpdateFailed": "Nepodařilo se aktualizovat podrobnosti o položce", + "ToastItemDetailsUpdateSuccess": "Podrobnosti o položce byly aktualizovány", + "ToastItemDetailsUpdateUnneeded": "Podrobnosti o položce nejsou potřeba aktualizovat", + "ToastItemMarkedAsFinishedFailed": "Nepodařilo se označit jako dokončené", + "ToastItemMarkedAsFinishedSuccess": "Položka označena jako dokončená", + "ToastItemMarkedAsNotFinishedFailed": "Nepodařilo se označit jako nedokončené", + "ToastItemMarkedAsNotFinishedSuccess": "Položka označena jako nedokončená", + "ToastLibraryCreateFailed": "Vytvoření knihovny se nezdařilo", + "ToastLibraryCreateSuccess": "Knihovna \"{0}\" vytvořena", + "ToastLibraryDeleteFailed": "Nepodařilo se smazat knihovnu", + "ToastLibraryDeleteSuccess": "Knihovna smazána", + "ToastLibraryScanFailedToStart": "Nepodařilo se spustit kontrolu", + "ToastLibraryScanStarted": "Kontrola knihovny spuštěna", + "ToastLibraryUpdateFailed": "Aktualizace knihovny se nezdařila", + "ToastLibraryUpdateSuccess": "Knihovna \"{0}\" aktualizována", + "ToastPlaylistCreateFailed": "Vytvoření seznamu skladeb se nezdařilo", + "ToastPlaylistCreateSuccess": "Seznam skladeb vytvořen", + "ToastPlaylistRemoveFailed": "Nepodařilo se odstranit seznam skladeb", + "ToastPlaylistRemoveSuccess": "Seznam skladeb odstraněn", + "ToastPlaylistUpdateFailed": "Aktualizace seznamu skladeb se nezdařila", + "ToastPlaylistUpdateSuccess": "Seznam skladeb aktualizován", + "ToastPodcastCreateFailed": "Vytvoření podcastu se nezdařilo", + "ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen", + "ToastRemoveItemFromCollectionFailed": "Nepodařilo se odebrat položku z kolekce", + "ToastRemoveItemFromCollectionSuccess": "Položka odstraněna z kolekce", + "ToastRSSFeedCloseFailed": "Nepodařilo se zavřít RSS kanál", + "ToastRSSFeedCloseSuccess": "RSS kanál uzavřen", + "ToastSendEbookToDeviceFailed": "Odeslání e-knihy do zařízení se nezdařilo", + "ToastSendEbookToDeviceSuccess": "E-kniha odeslána do zařízení \"{0}\"", + "ToastSeriesUpdateFailed": "Aktualizace řady se nezdařila", + "ToastSeriesUpdateSuccess": "Aktualizace série byla úspěšná", + "ToastSessionDeleteFailed": "Nepodařilo se smazat relaci", + "ToastSessionDeleteSuccess": "Relace smazána", + "ToastSocketConnected": "Zásuvka připojena", + "ToastSocketDisconnected": "Zásuvka odpojena", + "ToastSocketFailedToConnect": "Socket se nepodařilo připojit", + "ToastUserDeleteFailed": "Nepodařilo se smazat uživatele", + "ToastUserDeleteSuccess": "Uživatel smazán" +} From 5f035db0a97ed34df0850f15b162e9c49c10a814 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 1 Nov 2023 15:29:58 -0500 Subject: [PATCH 107/208] Rename cs-CZ.json to cs.json --- client/strings/{cs-CZ.json => cs.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/strings/{cs-CZ.json => cs.json} (100%) diff --git a/client/strings/cs-CZ.json b/client/strings/cs.json similarity index 100% rename from client/strings/cs-CZ.json rename to client/strings/cs.json From 2eff69fe9ffa48a827fb771e56af08ab1df1a82d Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 1 Nov 2023 15:42:54 -0500 Subject: [PATCH 108/208] Add czech translation to dropdown --- client/plugins/i18n.js | 1 + client/strings/cs.json | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index f404bb80a..9a7eb02e8 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -5,6 +5,7 @@ import { supplant } from './utils' const defaultCode = 'en-us' const languageCodeMap = { + 'cs': { label: 'Čeština', dateFnsLocale: 'cs' }, 'da': { label: 'Dansk', dateFnsLocale: 'da' }, 'de': { label: 'Deutsch', dateFnsLocale: 'de' }, 'en-us': { label: 'English', dateFnsLocale: 'enUS' }, diff --git a/client/strings/cs.json b/client/strings/cs.json index 06e019461..0adbfcc70 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -198,7 +198,7 @@ "LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování", "LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups", "LabelBackupsMaxBackupSize": "Maximální velikost zálohy (v GB)", - "LabelBackupsMaxBackupSizeHelp": "Jako pojistka proti chybné konfiguraci se zálohy nezdaří, pokud překročí nastavenou velikost.", + "LabelBackupsMaxBackupSizeHelp": "Jako pojistka proti chybné konfiguraci se zálohy nezdaří, pokud překročí nastavenou velikost.", "LabelBackupsNumberToKeep": "Počet záloh, které se mají uchovat", "LabelBackupsNumberToKeepHelp": "Najednou bude odstraněna pouze 1 záloha, takže pokud již máte více záloh, měli byste je odstranit ručně.", "LabelBitrate": "Datový tok", @@ -426,7 +426,7 @@ "LabelSettingsHomePageBookshelfView": "Domovská stránka používá regálové zobrazení", "LabelSettingsLibraryBookshelfView": "Knihovna používá regálové zobrazení", "LabelSettingsParseSubtitles": "Analyzovat titulky", -"LabelSettingsParseSubtitlesHelp": "Extrahovat titulky z názvů složek audioknih.<br>Titulky musí být odděleny znakem \" - \"<br>tj. \"Název knihy - Zde titulky\" má podtitul \"Zde titulky\"", + "LabelSettingsParseSubtitlesHelp": "Extrahovat titulky z názvů složek audioknih.<br>Titulky musí být odděleny znakem \" - \"<br>tj. \"Název knihy - Zde titulky\" má podtitul \"Zde titulky\"", "LabelSettingsPreferMatchedMetadata": "Preferovat odpovídající metadata", "LabelSettingsPreferMatchedMetadataHelp": "Shodná data přepíší detaily položky při použití Rychlého výběru. Ve výchozím nastavení funkce Quick Match vyplní pouze chybějící podrobnosti.", "LabelSettingsSkipMatchingBooksWithASIN": "Přeskočit odpovídající knihy, které již mají ASIN", @@ -640,7 +640,7 @@ "MessageWatcherIsDisabledGlobally": "Watcher je globálně zakázán v nastavení serveru", "MessageXLibraryIsEmpty": "{0} knihovna je prázdná!", "MessageYourAudiobookDurationIsLonger": "Doba trvání audioknihy je delší než nalezená délka", - "MessageYourAudiobookDurationIsShorter": "Délka audioknihy je kratší, než byla nalezena." + "MessageYourAudiobookDurationIsShorter": "Délka audioknihy je kratší, než byla nalezena.", "NoteChangeRootPassword": "Uživatel root je jediný uživatel, který může mít prázdné heslo", "NoteChapterEditorTimes": "Poznámka: Čas začátku první kapitoly musí zůstat v 0:00 a čas začátku poslední kapitoly nesmí překročit tuto dobu trvání audioknihy.", "NoteFolderPicker": "Poznámka: složky, které jsou již namapovány, nebudou zobrazeny", @@ -726,4 +726,4 @@ "ToastSocketFailedToConnect": "Socket se nepodařilo připojit", "ToastUserDeleteFailed": "Nepodařilo se smazat uživatele", "ToastUserDeleteSuccess": "Uživatel smazán" -} +} \ No newline at end of file From 31004376512b72b81d6fe92550afa0d1087fc461 Mon Sep 17 00:00:00 2001 From: Plazec <plazec82@gmail.com> Date: Thu, 2 Nov 2023 19:44:32 +0100 Subject: [PATCH 109/208] Update cs.json Corrected some translation errors and made the translation more consistent. --- client/strings/cs.json | 294 ++++++++++++++++++++--------------------- 1 file changed, 147 insertions(+), 147 deletions(-) diff --git a/client/strings/cs.json b/client/strings/cs.json index 0adbfcc70..07f3d4f7d 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -17,9 +17,9 @@ "ButtonChooseAFolder": "Vybrat složku", "ButtonChooseFiles": "Vybrat soubory", "ButtonClearFilter": "Vymazat filtr", - "ButtonCloseFeed": "Zavřít podávání", - "ButtonCollections": "Sbírky", - "ButtonConfigureScanner": "Konfigurovat skener", + "ButtonCloseFeed": "Zavřít kanál", + "ButtonCollections": "Kolekce", + "ButtonConfigureScanner": "Konfigurovat Prohledávání", "ButtonCreate": "Vytvořit", "ButtonCreateBackup": "Vytvořit zálohu", "ButtonDelete": "Smazat", @@ -27,7 +27,7 @@ "ButtonEdit": "Upravit", "ButtonEditChapters": "Upravit kapitoly", "ButtonEditPodcast": "Upravit podcast", - "ButtonForceReScan": "Vynutit opětovné skenování", + "ButtonForceReScan": "Vynutit opětovné prohledání", "ButtonFullPath": "Úplná cesta", "ButtonHide": "Skrýt", "ButtonHome": "Domů", @@ -36,10 +36,10 @@ "ButtonLibrary": "Knihovna", "ButtonLogout": "Odhlásit", "ButtonLookup": "Vyhledat", - "ButtonManageTracks": "Správa tras", + "ButtonManageTracks": "Správa stop", "ButtonMapChapterTitles": "Mapovat názvy kapitol", - "ButtonMatchAllAuthors": "Shoda se všemi autory", - "ButtonMatchBooks": "Knihy zápalek", + "ButtonMatchAllAuthors": "Spárovat všechny autory", + "ButtonMatchBooks": "Spárovat Knihy", "ButtonNevermind": "Nevadí", "ButtonOk": "Ok", "ButtonOpenFeed": "Otevřít kanál", @@ -47,28 +47,28 @@ "ButtonPlay": "Přehrát", "ButtonPlaying": "Hraje", "ButtonPlaylists": "Seznamy skladeb", - "ButtonPurgeAllCache": "Vymazat veškerou mezipaměť", - "ButtonPurgeItemsCache": "Vymazat mezipaměť položek", + "ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť", + "ButtonPurgeItemsCache": "Vyčistit mezipaměť položek", "ButtonPurgeMediaProgress": "Vyčistit průběh médií", "ButtonQueueAddItem": "Přidat do fronty", "ButtonQueueRemoveItem": "Odstranit z fronty", - "ButtonQuickMatch": "Rychlá shoda", + "ButtonQuickMatch": "Rychlé přiřazení", "ButtonRead": "Číst", "ButtonRemove": "Odstranit", "ButtonRemoveAll": "Odstranit vše", "ButtonRemoveAllLibraryItems": "Odstranit všechny položky knihovny", - "ButtonRemoveFromContinueListening": "Odstranit z pokračujícího poslechu", - "ButtonRemoveFromContinueReading": "Odstranit z pokračování ve čtení", - "ButtonRemoveSeriesFromContinueSeries": "Odstranit sérii z pokračování série", - "ButtonReScan": "Znovu skenovat", + "ButtonRemoveFromContinueListening": "Odstranit z Pokračovat v poslechu", + "ButtonRemoveFromContinueReading": "Odstranit z Pokračovat ve čtení", + "ButtonRemoveSeriesFromContinueSeries": "Odstranit sérii z Pokračovat v sérii", + "ButtonReScan": "Znovu prohledat", "ButtonReset": "Resetovat", "ButtonResetToDefault": "Obnovit výchozí", "ButtonRestore": "Obnovit", "ButtonSave": "Uložit", "ButtonSaveAndClose": "Uložit a zavřít", "ButtonSaveTracklist": "Uložit seznam skladeb", - "ButtonScan": "Skenovat", - "ButtonScanLibrary": "Knihovna skenů", + "ButtonScan": "Prohledat", + "ButtonScanLibrary": "Prohledat Knihovnu", "ButtonSearch": "Hledat", "ButtonSelectFolderPath": "Vybrat cestu ke složce", "ButtonSeries": "Série", @@ -95,9 +95,9 @@ "HeaderBackups": "Zálohy", "HeaderChangePassword": "Změnit heslo", "HeaderChapters": "Kapitoly", - "HeaderChooseAFolder": "Vyberte složku", + "HeaderChooseAFolder": "Zvolte složku", "HeaderCollection": "Kolekce", - "HeaderCollectionItems": "Položky sbírky", + "HeaderCollectionItems": "Položky kolekce", "HeaderCover": "Obálka", "HeaderCurrentDownloads": "Aktuální stahování", "HeaderDetails": "Podrobnosti", @@ -122,21 +122,21 @@ "HeaderListeningStats": "Statistiky poslechu", "HeaderLogin": "Přihlásit", "HeaderLogs": "Záznamy", - "HeaderManageGenres": "Správa žánrů", - "HeaderManageTags": "Správa značek", - "HeaderMapDetails": "Detaily mapy", + "HeaderManageGenres": "Spravovat žánry", + "HeaderManageTags": "Spravovat štítky", + "HeaderMapDetails": "Podrobnosti mapování", "HeaderMatch": "Shoda", "HeaderMetadataOrderOfPrecedence": "Pořadí priorit metadat", - "HeaderMetadataToEmbed": "Metadata pro vložení", + "HeaderMetadataToEmbed": "Metadata k vložení", "HeaderNewAccount": "Nový účet", "HeaderNewLibrary": "Nová knihovna", "HeaderNotifications": "Oznámení", "HeaderOpenRSSFeed": "Otevřít RSS kanál", "HeaderOtherFiles": "Ostatní soubory", "HeaderPermissions": "Oprávnění", - "HeaderPlayerQueue": "Fronta hráčů", + "HeaderPlayerQueue": "Fronta přehrávače", "HeaderPlaylist": "Seznam skladeb", - "HeaderPlaylistItems": "Položky playlistu", + "HeaderPlaylistItems": "Položky seznamu přehrávání", "HeaderPodcastsToAdd": "Podcasty k přidání", "HeaderPreviewCover": "Náhled obálky", "HeaderRemoveEpisode": "Odstranit epizodu", @@ -146,8 +146,8 @@ "HeaderRSSFeeds": "RSS kanály", "HeaderSavedMediaProgress": "Průběh uložených médií", "HeaderSchedule": "Plán", - "HeaderScheduleLibraryScans": "Naplánovat automatické skenování knihoven", - "HeaderSession": "Session", + "HeaderScheduleLibraryScans": "Naplánovat automatické prohledávání knihoven", + "HeaderSession": "Relace", "HeaderSetBackupSchedule": "Nastavit plán zálohování", "HeaderSettings": "Nastavení", "HeaderSettingsDisplay": "Zobrazit", @@ -165,7 +165,7 @@ "HeaderTools": "Nástroje", "HeaderUpdateAccount": "Aktualizovat účet", "HeaderUpdateAuthor": "Aktualizovat autora", - "HeaderUpdateDetails": "Podrobnosti o aktualizaci", + "HeaderUpdateDetails": "Aktualizovat podrobnosti", "HeaderUpdateLibrary": "Aktualizovat knihovnu", "HeaderUsers": "Uživatelé", "HeaderYourStats": "Vaše statistiky", @@ -179,8 +179,8 @@ "LabelAddedAt": "Přidáno v", "LabelAddToCollection": "Přidat do kolekce", "LabelAddToCollectionBatch": "Přidat {0} knihy do kolekce", - "LabelAddToPlaylist": "Přidat do playlistu", - "LabelAddToPlaylistBatch": "Přidat {0} položky do playlistu", + "LabelAddToPlaylist": "Přidat do seznamu přehrávání", + "LabelAddToPlaylistBatch": "Přidat {0} položky do seznamu přehrávání", "LabelAdminUsersOnly": "Pouze administrátoři", "LabelAll": "Vše", "LabelAllUsers": "Všichni uživatelé", @@ -189,16 +189,16 @@ "LabelAlreadyInYourLibrary": "Již ve vaší knihovně", "LabelAppend": "Připojit", "LabelAuthor": "Autor", - "LabelAuthorFirstLast": "Autor (první poslední)", - "LabelAuthorLastFirst": "Autor (poslední, první)", + "LabelAuthorFirstLast": "Autor (jméno a příjmení)", + "LabelAuthorLastFirst": "Autor (příjmení a jméno)", "LabelAuthors": "Autoři", - "LabelAutoDownloadEpisodes": "Automatické stahování epizod", + "LabelAutoDownloadEpisodes": "Automaticky stahovat epizody", "LabelBackToUser": "Zpět k uživateli", "LabelBackupLocation": "Umístění zálohy", "LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování", "LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups", "LabelBackupsMaxBackupSize": "Maximální velikost zálohy (v GB)", - "LabelBackupsMaxBackupSizeHelp": "Jako pojistka proti chybné konfiguraci se zálohy nezdaří, pokud překročí nastavenou velikost.", + "LabelBackupsMaxBackupSizeHelp": "Ochrana proti chybné konfiguraci: Zálohování se nezdaří, pokud překročí nastavenou velikost.", "LabelBackupsNumberToKeep": "Počet záloh, které se mají uchovat", "LabelBackupsNumberToKeepHelp": "Najednou bude odstraněna pouze 1 záloha, takže pokud již máte více záloh, měli byste je odstranit ručně.", "LabelBitrate": "Datový tok", @@ -212,20 +212,20 @@ "LabelClosePlayer": "Zavřít přehrávač", "LabelCodec": "Kodek", "LabelCollapseSeries": "Sbalit sérii", - "LabelCollection": "Sbírka", - "LabelCollections": "Sbírky", + "LabelCollection": "Kolekce", + "LabelCollections": "Kolekce", "LabelComplete": "Dokončeno", "LabelConfirmPassword": "Potvrdit heslo", "LabelContinueListening": "Pokračovat v poslechu", "LabelContinueReading": "Pokračovat ve čtení", - "LabelContinueSeries": "Pokračovat v řadě", + "LabelContinueSeries": "Pokračovat v sérii", "LabelCover": "Obálka", - "LabelCoverImageURL": "URL obálkového obrázku", + "LabelCoverImageURL": "URL obrázku obálky", "LabelCreatedAt": "Vytvořeno v", - "LabelCronExpression": "Cron výraz", + "LabelCronExpression": "Výraz Cronu", "LabelCurrent": "Aktuální", "LabelCurrently": "Aktuálně:", - "LabelCustomCronExpression": "Vlastní cron výraz:", + "LabelCustomCronExpression": "Vlastní výraz cronu:", "LabelDatetime": "Datum a čas", "LabelDeleteFromFileSystemCheckbox": "Smazat ze souborového systému (zrušte zaškrtnutí pro odstranění pouze z databáze)", "LabelDescription": "Popis", @@ -242,14 +242,14 @@ "LabelDuration": "Doba trvání", "LabelDurationFound": "Doba trvání nalezena:", "LabelEbook": "Elektronická kniha", - "LabelEbooks": "E-knihy", + "LabelEbooks": "Elektronické knihy", "LabelEdit": "Upravit", "LabelEmail": "E-mail", "LabelEmailSettingsFromAddress": "Z adresy", "LabelEmailSettingsSecure": "Zabezpečené", "LabelEmailSettingsSecureHelp": "Pokud je true, připojení bude při připojování k serveru používat TLS. Pokud je false, použije se protokol TLS, pokud server podporuje rozšíření STARTTLS. Ve většině případů nastavte tuto hodnotu na true, pokud se připojujete k portu 465. Pro port 587 nebo 25 ponechte hodnotu false. (z nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Testovací adresa", - "LabelEmbeddedCover": "Vložený obal", + "LabelEmbeddedCover": "Vložená obálka", "LabelEnable": "Povolit", "LabelEnd": "Konec", "LabelEpisode": "Epizoda", @@ -259,7 +259,7 @@ "LabelExplicit": "Explicitní", "LabelFeedURL": "URL zdroje", "LabelFile": "Soubor", - "LabelFileBirthtime": "Čas narození souboru", + "LabelFileBirthtime": "Čas vzniku souboru", "LabelFileModified": "Soubor změněn", "LabelFilename": "Název souboru", "LabelFilterByUser": "Filtrovat podle uživatele", @@ -274,12 +274,12 @@ "LabelGenres": "Žánry", "LabelHardDeleteFile": "Trvale smazat soubor", "LabelHasEbook": "Obsahuje elektronickou knihu", - "LabelHasSupplementaryEbook": "Obsahuje doplňkovou e-knihu", + "LabelHasSupplementaryEbook": "Obsahuje doplňkovou elektronickou knihu", "LabelHost": "Hostitel", "LabelHour": "Hodina", "LabelIcon": "Ikona", "LabelImageURLFromTheWeb": "URL obrázku z webu", - "LabelIncludeInTracklist": "Zahrnout do seznamu skladeb", + "LabelIncludeInTracklist": "Zahrnout do seznamu stop", "LabelIncomplete": "Neúplné", "LabelInProgress": "Probíhá", "LabelInterval": "Interval", @@ -296,7 +296,7 @@ "LabelItem": "Položka", "LabelLanguage": "Jazyk", "LabelLanguageDefaultServer": "Výchozí jazyk serveru", - "LabelLastBookAdded": "Přidán poslední kniha", + "LabelLastBookAdded": "Poslední kniha přidána", "LabelLastBookUpdated": "Poslední kniha aktualizována", "LabelLastSeen": "Naposledy viděno", "LabelLastTime": "Naposledy", @@ -309,7 +309,7 @@ "LabelLibrary": "Knihovna", "LabelLibraryItem": "Položka knihovny", "LabelLibraryName": "Název knihovny", - "LabelLimit": "Limit", + "LabelLimit": "Omezit", "LabelLineSpacing": "Řádkování", "LabelListenAgain": "Poslouchat znovu", "LabelLogLevelDebug": "Ladit", @@ -320,21 +320,21 @@ "LabelMediaType": "Typ média", "LabelMetadataOrderOfPrecedenceDescription": "1 je nejnižší priorita, 5 je nejvyšší priorita", "LabelMetadataProvider": "Poskytovatel metadat", - "LabelMetaTag": "Meta tag", - "LabelMetaTags": "Meta tagy", + "LabelMetaTag": "Metaznačka", + "LabelMetaTags": "Metaznačky", "LabelMinute": "Minuta", - "LabelMissing": "Chybí", + "LabelMissing": "Chybějící", "LabelMissingParts": "Chybějící díly", "LabelMore": "Více", "LabelMoreInfo": "Více informací", "LabelName": "Jméno", - "LabelNarrator": "Předčítání", - "LabelNarrators": "Předčítání", + "LabelNarrator": "Interpret", + "LabelNarrators": "Interpreti", "LabelNew": "Nový", "LabelNewestAuthors": "Nejnovější autoři", "LabelNewestEpisodes": "Nejnovější epizody", "LabelNewPassword": "Nové heslo", - "LabelNextBackupDate": "Datum další zálohy", + "LabelNextBackupDate": "Datum příští zálohy", "LabelNextScheduledRun": "Další naplánované spuštění", "LabelNoEpisodesSelected": "Nebyly vybrány žádné epizody", "LabelNotes": "Poznámky", @@ -348,9 +348,9 @@ "LabelNotificationsMaxQueueSize": "Maximální velikost fronty pro oznamovací události", "LabelNotificationsMaxQueueSizeHelp": "Události jsou omezeny na 1 za sekundu. Události budou ignorovány, pokud je fronta v maximální velikosti. Tím se zabrání spamování oznámení.", "LabelNotificationTitleTemplate": "Šablona názvu", - "LabelNotStarted": "Nespuštěno", + "LabelNotStarted": "Nezahájeno", "LabelNumberOfBooks": "Počet knih", - "LabelNumberOfEpisodes": "# epizod", + "LabelNumberOfEpisodes": "Počet epizod", "LabelOpenRSSFeed": "Otevřít RSS kanál", "LabelOverwrite": "Přepsat", "LabelPassword": "Heslo", @@ -359,9 +359,9 @@ "LabelPermissionsAccessAllTags": "Má přístup ke všem značkám", "LabelPermissionsAccessExplicitContent": "Má přístup k explicitnímu obsahu", "LabelPermissionsDelete": "Může mazat", - "LabelPermissionsDownload": "Lze stáhnout", + "LabelPermissionsDownload": "Může stahovat", "LabelPermissionsUpdate": "Může aktualizovat", - "LabelPermissionsUpload": "Lze nahrávat", + "LabelPermissionsUpload": "Může nahrávat", "LabelPhotoPathURL": "Cesta k fotografii/URL", "LabelPlaylists": "Seznamy skladeb", "LabelPlayMethod": "Metoda přehrávání", @@ -370,30 +370,30 @@ "LabelPodcastType": "Typ podcastu", "LabelPort": "Port", "LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)", - "LabelPreventIndexing": "Zabraňte indexování vašeho zdroje adresáři podcastů iTunes a Google", + "LabelPreventIndexing": "Zabránit indexování vašeho kanálu v adresářích podcastů iTunes a Google", "LabelPrimaryEbook": "Hlavní e-kniha", "LabelProgress": "Průběh", "LabelProvider": "Poskytovatel", - "LabelPubDate": "Datum hospody", + "LabelPubDate": "Datum vydání", "LabelPublisher": "Vydavatel", - "LabelPublishYear": "Rok publikování", + "LabelPublishYear": "Rok vydání", "LabelRead": "Číst", "LabelReadAgain": "Číst znovu", "LabelReadEbookWithoutProgress": "Číst e-knihu bez zachování průběhu", - "LabelRecentlyAdded": "Nedávno přidáno", - "LabelRecentSeries": "Poslední série", + "LabelRecentlyAdded": "Nedávno přidané", + "LabelRecentSeries": "Nedávné série", "LabelRecommended": "Doporučeno", "LabelRegion": "Region", "LabelReleaseDate": "Datum vydání", - "LabelRemoveCover": "Sejmout kryt", + "LabelRemoveCover": "Odstranit obálku", "LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka", "LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka", "LabelRSSFeedOpen": "Otevření RSS kanálu", "LabelRSSFeedPreventIndexing": "Zabránit indexování", "LabelRSSFeedSlug": "RSS kanál Slug", "LabelRSSFeedURL": "URL RSS kanálu", - "LabelSearchTerm": "Hledaný výraz", - "LabelSearchTitle": "Název vyhledávání", + "LabelSearchTerm": "Vyhledat termín", + "LabelSearchTitle": "Vyhledat název", "LabelSearchTitleOrASIN": "Vyhledat název nebo ASIN", "LabelSeason": "Sezóna", "LabelSelectAllEpisodes": "Vybrat všechny epizody", @@ -416,26 +416,26 @@ "LabelSettingsDisableWatcherHelp": "Zakáže automatické přidávání/aktualizaci položek při zjištění změn v souboru. *Vyžaduje restart serveru", "LabelSettingsEnableWatcher": "Povolit sledování", "LabelSettingsEnableWatcherForLibrary": "Povolit sledování složky pro knihovnu", - "LabelSettingsEnableWatcherHelp": "Umožňuje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru", + "LabelSettingsEnableWatcherHelp": "Povoluje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru", "LabelSettingsExperimentalFeatures": "Experimentální funkce", - "LabelSettingsExperimentalFeaturesHelp": "Vyvíjené funkce, které by mohly využít vaši zpětnou vazbu a pomoc s testováním. Kliknutím otevřete diskuzi na githubu.", + "LabelSettingsExperimentalFeaturesHelp": "Funkce ve vývoji, které by mohly využít vaši zpětnou vazbu a pomoc s testováním. Kliknutím otevřete diskuzi na githubu.", "LabelSettingsFindCovers": "Najít obálky", - "LabelSettingsFindCoversHelp": "Pokud vaše audiokniha nemá vloženou obálku nebo obrázek obálky uvnitř složky, skener se pokusí obálku najít.<br>Poznámka: Tím se prodlouží doba skenování", - "LabelSettingsHideSingleBookSeries": "Skrýt sérii jednotlivých knih", - "LabelSettingsHideSingleBookSeriesHelp": "Série, které mají jednu knihu, budou skryty na stránce série a na domovské stránce.", - "LabelSettingsHomePageBookshelfView": "Domovská stránka používá regálové zobrazení", - "LabelSettingsLibraryBookshelfView": "Knihovna používá regálové zobrazení", - "LabelSettingsParseSubtitles": "Analyzovat titulky", - "LabelSettingsParseSubtitlesHelp": "Extrahovat titulky z názvů složek audioknih.<br>Titulky musí být odděleny znakem \" - \"<br>tj. \"Název knihy - Zde titulky\" má podtitul \"Zde titulky\"", - "LabelSettingsPreferMatchedMetadata": "Preferovat odpovídající metadata", - "LabelSettingsPreferMatchedMetadataHelp": "Shodná data přepíší detaily položky při použití Rychlého výběru. Ve výchozím nastavení funkce Quick Match vyplní pouze chybějící podrobnosti.", - "LabelSettingsSkipMatchingBooksWithASIN": "Přeskočit odpovídající knihy, které již mají ASIN", - "LabelSettingsSkipMatchingBooksWithISBN": "Přeskočit odpovídající knihy, které již mají ISBN", - "LabelSettingsSortingIgnorePrefixes": "Ignorovat prefixy při třídění", + "LabelSettingsFindCoversHelp": "Pokud vaše audiokniha nemá vloženou obálku nebo obrázek obálky uvnitř složky, skener se pokusí obálku najít.<br>Poznámka: Tím se prodlouží doba prohledávání", + "LabelSettingsHideSingleBookSeries": "Skrýt sérii s jedinou knihou", + "LabelSettingsHideSingleBookSeriesHelp": "Série, které mají jedinou knihu, budou skryty na stránce série a na domovské stránce.", + "LabelSettingsHomePageBookshelfView": "Domovská stránka používá zobrazení police s knihami", + "LabelSettingsLibraryBookshelfView": "Knihovna používá zobrazení police s knihami", + "LabelSettingsParseSubtitles": "Analzyovat podtitul", + "LabelSettingsParseSubtitlesHelp": "Rozparsovat podtitul z názvů složek audioknih.<br>Podtiul musí být oddělen znakem \" - \"<br>tj. \"Název knihy - Zde Podtitul\" má podtitul \"Zde podtitul\"", + "LabelSettingsPreferMatchedMetadata": "Preferovat spárovaná metadata", + "LabelSettingsPreferMatchedMetadataHelp": "Spárovaná data budou mít při použití funkce Rychlé párování přednost před údaji o položce. Ve výchozím nastavení funkce Rychlé párování pouze doplní chybějící údaje.", + "LabelSettingsSkipMatchingBooksWithASIN": "Přeskočit párování knih, které již mají ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Přeskočit párování knih, které již mají ISBN", + "LabelSettingsSortingIgnorePrefixes": "Ignorovat předpony při třídění", "LabelSettingsSortingIgnorePrefixesHelp": "tj. pro předponu \"the\" název knihy \"Název knihy\" by se třídil jako \"Název knihy, The\"", "LabelSettingsSquareBookCovers": "Použít čtvercové obálky knih", - "LabelSettingsSquareBookCoversHelp": "Preferovat použití čtvercových desek před standardními obálkami 1.6:1", - "LabelSettingsStoreCoversWithItem": "Uložit obaly s položkou", + "LabelSettingsSquareBookCoversHelp": "Preferovat použití čtvercových obálek před standardními obálkami 1.6:1", + "LabelSettingsStoreCoversWithItem": "Uložit obálky s položkou", "LabelSettingsStoreCoversWithItemHelp": "Ve výchozím nastavení jsou obálky uloženy v adresáři /metadata/items, povolením tohoto nastavení se obálky uloží do složky položek knihovny. Zůstane zachován pouze jeden soubor s názvem \"cover\"", "LabelSettingsStoreMetadataWithItem": "Uložit metadata s položkou", "LabelSettingsStoreMetadataWithItemHelp": "Ve výchozím nastavení jsou soubory metadat uloženy v adresáři /metadata/items, povolením tohoto nastavení budou soubory metadat uloženy ve složkách položek knihovny", @@ -443,52 +443,52 @@ "LabelShowAll": "Zobrazit vše", "LabelSize": "Velikost", "LabelSleepTimer": "Časovač vypnutí", - "LabelSlug": "Slimák", + "LabelSlug": "Slug", "LabelStart": "Spustit", "LabelStarted": "Spuštěno", "LabelStartedAt": "Spuštěno v", - "LabelStartTime": "Čas zahájení", + "LabelStartTime": "Čas Spuštění", "LabelStatsAudioTracks": "Zvukové stopy", "LabelStatsAuthors": "Autoři", "LabelStatsBestDay": "Nejlepší den", "LabelStatsDailyAverage": "Denní průměr", "LabelStatsDays": "Dny", - "LabelStatsDaysListened": "Dny odposloucháváno", + "LabelStatsDaysListened": "Dny poslechu", "LabelStatsHours": "Hodiny", "LabelStatsInARow": "v řadě", - "LabelStatsItemsFinished": "Položky dokončeny", + "LabelStatsItemsFinished": "Dokončené Položky", "LabelStatsItemsInLibrary": "Položky v knihovně", "LabelStatsMinutes": "minut", "LabelStatsMinutesListening": "Minuty poslechu", "LabelStatsOverallDays": "Celkový počet dní", - "LabelStatsOverallHours": "Celkové hodiny", + "LabelStatsOverallHours": "Celkový počet hodin", "LabelStatsWeekListening": "Týdenní poslech", - "LabelSubtitle": "Titulky", + "LabelSubtitle": "Podtitul", "LabelSupportedFileTypes": "Podporované typy souborů", "LabelTag": "Značka", "LabelTags": "Značky", - "LabelTagsAccessibleToUser": "Tagy přístupné uživateli", - "LabelTagsNotAccessibleToUser": "Značky nejsou přístupné uživateli", - "LabelTasks": "Úlohy běží", + "LabelTagsAccessibleToUser": "Značky přístupné uživateli", + "LabelTagsNotAccessibleToUser": "Značky nepřístupné uživateli", + "LabelTasks": "Spuštěné Úlohy", "LabelTheme": "Téma", - "LabelThemeDark": "Tmavý", - "LabelThemeLight": "Světlo", + "LabelThemeDark": "Tmavé", + "LabelThemeLight": "Světlé", "LabelTimeBase": "Časová základna", - "LabelTimeListened": "Poslouchaný čas", - "LabelTimeListenedToday": "Čas poslouchaný dnes", - "LabelTimeRemaining": "{0} zbývající", + "LabelTimeListened": "Čas poslechu", + "LabelTimeListenedToday": "Čas poslechu dnes", + "LabelTimeRemaining": "{0} zbývá", "LabelTimeToShift": "Čas posunu v sekundách", "LabelTitle": "Název", "LabelToolsEmbedMetadata": "Vložit metadata", - "LabelToolsEmbedMetadataDescription": "Vložit metadata do zvukových souborů včetně titulního obrázku a kapitoly.", + "LabelToolsEmbedMetadataDescription": "Vložit metadata do zvukových souborů včetně obálky a kapitol.", "LabelToolsMakeM4b": "Vytvořit soubor audioknihy M4B", - "LabelToolsMakeM4bDescription": "Vygenerovat soubor . Soubor audioknihy M4B s vloženými metadaty, titulním obrázkem a Kapitoly.", + "LabelToolsMakeM4bDescription": "Vygenerovat soubor audioknihy M4B s vloženými metadaty, obálkou a kapitolami.", "LabelToolsSplitM4b": "Rozdělit M4B na MP3", - "LabelToolsSplitM4bDescription": "Vytvořit MP3 z M4B splitu od Kapitoly s vloženými metadaty, obrázkem obálky a Kapitoly.", + "LabelToolsSplitM4bDescription": "Vytvořit soubory MP3 z M4B rozděleného podle kapitol s vloženými metadaty, obrázku obálky a kapitol.", "LabelTotalDuration": "Celková doba trvání", - "LabelTotalTimeListened": "Celkový poslouchaný čas", + "LabelTotalTimeListened": "Celkový čas poslechu", "LabelTrackFromFilename": "Stopa z názvu souboru", - "LabelTrackFromMetadata": "Sledovat z metadat", + "LabelTrackFromMetadata": "Stopa z metadat", "LabelTracks": "Stopy", "LabelTracksMultiTrack": "Více stop", "LabelTracksNone": "Žádné stopy", @@ -499,47 +499,47 @@ "LabelUpdateCover": "Aktualizovat obálku", "LabelUpdateCoverHelp": "Povolit přepsání existujících obálek pro vybrané knihy, pokud je nalezena shoda", "LabelUpdatedAt": "Aktualizováno v", - "LabelUpdateDetails": "Podrobnosti o aktualizaci", + "LabelUpdateDetails": "Aktualizovat podrobnosti", "LabelUpdateDetailsHelp": "Povolit přepsání existujících údajů o vybraných knihách, když je nalezena shoda", "LabelUploaderDragAndDrop": "Přetáhnout soubory nebo složky", "LabelUploaderDropFiles": "Odstranit soubory", - "LabelUseChapterTrack": "Použít stopu kapitol", - "LabelUseFullTrack": "Použít celou trasu", + "LabelUseChapterTrack": "Použít stopu kapitoly", + "LabelUseFullTrack": "Použít celou stopu", "LabelUser": "Uživatel", "LabelUsername": "Uživatelské jméno", "LabelValue": "Hodnota", "LabelVersion": "Verze", "LabelViewBookmarks": "Zobrazit záložky", "LabelViewChapters": "Zobrazit kapitoly", - "LabelViewQueue": "Zobrazit frontu hráčů", - "LabelVolume": "Svazek", + "LabelViewQueue": "Zobrazit frontu přehrávače", + "LabelVolume": "Hlasitost", "LabelWeekdaysToRun": "Dny v týdnu ke spuštění", "LabelYourAudiobookDuration": "Doba trvání vaší audioknihy", "LabelYourBookmarks": "Vaše záložky", - "LabelYourPlaylists": "Vaše playlisty", + "LabelYourPlaylists": "Vaše seznamy přehrávání", "LabelYourProgress": "Váš pokrok", - "MessageAddToPlayerQueue": "Přidat do fronty hráčů", + "MessageAddToPlayerQueue": "Přidat do fronty přehrávače", "MessageAppriseDescription": "Abyste mohli používat tuto funkci, musíte mít spuštěnou instanci <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nebo API, které bude zpracovávat stejné požadavky. <br />Adresa URL API Apprise by měla být úplná URL cesta pro odeslání oznámení, např. pokud je vaše instance API obsluhována na adrese <code>http://192.168.1.1:8337</code> pak byste měli zadat <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Zálohy zahrnují uživatele, průběh uživatele, podrobnosti o položkách knihovny, nastavení serveru a obrázky uložené v <code>/metadata/items</code> a <code>/metadata/authors</code>. Zálohy <strong>ne</strong> zahrnují všechny soubory uložené ve složkách knihovny.", - "MessageBatchQuickMatchDescription": "Rychlá shoda se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlá shoda přepsat stávající obálky a/nebo metadata.", + "MessageBatchQuickMatchDescription": "Rychlá párování se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlé párování přepsat stávající obálky a/nebo metadata.", "MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku", "MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"", "MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály", "MessageBookshelfNoSeries": "Nemáte žádnou sérii", - "MessageChapterEndIsAfter": "Konec kapitoly je po skončení audioknihy", + "MessageChapterEndIsAfter": "Konec kapitoly přesahuje konec audioknihy", "MessageChapterErrorFirstNotZero": "První kapitola musí začínat na 0", - "MessageChapterErrorStartGteDuration": "Neplatný čas začátku musí být kratší než doba trvání audioknihy", - "MessageChapterErrorStartLtPrev": "Neplatný čas začátku musí být větší nebo roven času začátku předchozí kapitoly", - "MessageChapterStartIsAfter": "Začátek kapitoly je po skončení audioknihy", + "MessageChapterErrorStartGteDuration": "Neplatný čas začátku, musí být kratší než doba trvání audioknihy", + "MessageChapterErrorStartLtPrev": "Neplatný čas začátku, musí být větší nebo roven času začátku předchozí kapitoly", + "MessageChapterStartIsAfter": "Začátek kapitoly přesahuje konec audioknihy", "MessageCheckingCron": "Kontrola cronu...", "MessageConfirmCloseFeed": "Opravdu chcete zavřít tento kanál?", "MessageConfirmDeleteBackup": "Opravdu chcete smazat zálohu pro {0}?", - "MessageConfirmDeleteFile": "Tím dojde ke smazání souboru ze souborového systému. Jsi si jistý?", + "MessageConfirmDeleteFile": "Tento krok smaže soubor ze souborového systému. Jsi si jisti?", "MessageConfirmDeleteLibrary": "Opravdu chcete trvale smazat knihovnu \"{0}\"?", - "MessageConfirmDeleteLibraryItem": "Tím se odstraní položka knihovny z databáze a souborového systému. Jsi si jistý?", - "MessageConfirmDeleteLibraryItems": "Toto smaže {0} položky knihovny z databáze a vašeho souborového systému. Jsi si jistý?", + "MessageConfirmDeleteLibraryItem": "Tento krok odstraní položku knihovny z databáze a vašeho souborového systému. Jste si jisti?", + "MessageConfirmDeleteLibraryItems": "Tímto smažete {0} položkek knihovny z databáze a vašeho souborového systému. Jsi si jisti?", "MessageConfirmDeleteSession": "Opravdu chcete smazat tuto relaci?", - "MessageConfirmForceReScan": "Opravdu chcete vynutit opětovnou kontrolu?", + "MessageConfirmForceReScan": "Opravdu chcete vynutit opětovné prohledání?", "MessageConfirmMarkAllEpisodesFinished": "Opravdu chcete označit všechny epizody jako dokončené?", "MessageConfirmMarkAllEpisodesNotFinished": "Opravdu chcete označit všechny epizody jako nedokončené?", "MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?", @@ -558,15 +558,15 @@ "MessageConfirmRenameTag": "Opravdu chcete přejmenovat tag \"{0}\" na \"{1}\" pro všechny položky?", "MessageConfirmRenameTagMergeNote": "Poznámka: Tato značka již existuje, takže budou sloučeny.", "MessageConfirmRenameTagWarning": "Varování! Podobná značka s jinými velkými a malými písmeny již existuje \"{0}\".", - "MessageConfirmReScanLibraryItems": "Opravdu chcete znovu zkontrolovat {0} položky?", + "MessageConfirmReScanLibraryItems": "Opravdu chcete znovu prohledat {0} položky?", "MessageConfirmSendEbookToDevice": "Opravdu chcete odeslat e-knihu {0} {1}\" do zařízení \"{2}\"?", "MessageDownloadingEpisode": "Stahuji epizodu", "MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop", "MessageEmbedFinished": "Vložení dokončeno!", "MessageEpisodesQueuedForDownload": "{0} epizody zařazené do fronty ke stažení", "MessageFeedURLWillBe": "URL zdroje bude {0}", - "MessageFetching": "Načítání...", - "MessageForceReScanDescription": "znovu otestuje všechny soubory jako novou kontrolu. Zvuk file ID3 tagy, OPF soubory a textové soubory budou naskenovány jako nové.", + "MessageFetching": "Stahování...", + "MessageForceReScanDescription": "znovu prohledá všechny soubory jako při novém skenování. ID3 tagy zvukových souborů OPF soubory a textové soubory budou skenovány jako nové.", "MessageImportantNotice": "Důležité upozornění!", "MessageInsertChapterBelow": "Vložit kapitolu níže", "MessageItemsSelected": "{0} vybraných položek", @@ -575,10 +575,10 @@ "MessageListeningSessionsInTheLastYear": "{0} poslechových relací za poslední rok", "MessageLoading": "Načítá se...", "MessageLoadingFolders": "Načítám složky...", - "MessageM4BFailed": "M4B se nezdařilo!", + "MessageM4BFailed": "M4B se nezdařil!", "MessageM4BFinished": "M4B dokončen!", - "MessageMapChapterTitles": "Mapování názvů kapitol na stávající audioknihu Kapitoly bez úpravy časových razítek", - "MessageMarkAllEpisodesFinished": "Označit všechny epizody za ukončené", + "MessageMapChapterTitles": "Mapování názvů kapitol ke stávajícím kapitolám audioknihy bez úpravy časových razítek", + "MessageMarkAllEpisodesFinished": "Označit všechny epizody za dokončené", "MessageMarkAllEpisodesNotFinished": "Označit všechny epizody jako nedokončené", "MessageMarkAsFinished": "Označit jako dokončené", "MessageMarkAsNotFinished": "Označit jako nedokončené", @@ -588,7 +588,7 @@ "MessageNoBackups": "Žádné zálohy", "MessageNoBookmarks": "Žádné záložky", "MessageNoChapters": "Žádné kapitoly", - "MessageNoCollections": "Žádné sbírky", + "MessageNoCollections": "Žádné kolekce", "MessageNoCoversFound": "Nebyly nalezeny žádné obálky", "MessageNoDescription": "Bez popisu", "MessageNoDownloadsInProgress": "Momentálně neprobíhá žádné stahování", @@ -597,47 +597,47 @@ "MessageNoEpisodes": "Žádné epizody", "MessageNoFoldersAvailable": "Nejsou k dispozici žádné složky", "MessageNoGenres": "Žádné žánry", - "MessageNoIssues": "Žádné problémy", + "MessageNoIssues": "Žádné výtisk", "MessageNoItems": "Žádné položky", "MessageNoItemsFound": "Nebyly nalezeny žádné položky", "MessageNoListeningSessions": "Žádné poslechové relace", "MessageNoLogs": "Žádné protokoly", - "MessageNoMediaProgress": "Žádný mediální průběh", + "MessageNoMediaProgress": "Žádný průběh médií", "MessageNoNotifications": "Žádná oznámení", "MessageNoPodcastsFound": "Nebyly nalezeny žádné podcasty", "MessageNoResults": "Žádné výsledky", "MessageNoSearchResultsFor": "Nebyly nalezeny žádné výsledky hledání pro \"{0}\"", - "MessageNoSeries": "Žádné řady", - "MessageNoTags": "Žádné tagy", + "MessageNoSeries": "Žádné série", + "MessageNoTags": "Žádné značky", "MessageNoTasksRunning": "Nejsou spuštěny žádné úlohy", "MessageNotYetImplemented": "Ještě není implementováno", "MessageNoUpdateNecessary": "Není nutná žádná aktualizace", "MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace", - "MessageNoUserPlaylists": "Nemáte žádné playlisty", + "MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb", "MessageOr": "nebo", "MessagePauseChapter": "Pozastavit přehrávání kapitoly", "MessagePlayChapter": "Poslechnout si začátek kapitoly", - "MessagePlaylistCreateFromCollection": "Vytvořit seznam skladeb ze sbírky", + "MessagePlaylistCreateFromCollection": "Vytvořit seznam skladeb z kolekce", "MessagePodcastHasNoRSSFeedForMatching": "Podcast nemá žádnou adresu URL kanálu RSS, kterou by mohl použít pro porovnávání", - "MessageQuickMatchDescription": "Vyplňte prázdné detaily položky a obálku prvním výsledkem shody z '{0}'. Nepřepisuje podrobnosti, pokud není povoleno nastavení serveru \"Preferovat odpovídající metadata\".", + "MessageQuickMatchDescription": "Vyplňte prázdné detaily položky a obálku prvním výsledkem shody z '{0}'. Nepřepisuje podrobnosti, pokud není povoleno nastavení serveru \"Preferovat párování metadata\".", "MessageRemoveChapter": "Odstranit kapitolu", "MessageRemoveEpisodes": "Odstranit {0} epizodu", - "MessageRemoveFromPlayerQueue": "Odstranit z fronty hráčů", + "MessageRemoveFromPlayerQueue": "Odstranit z fronty přehrávače", "MessageRemoveUserWarning": "Opravdu chcete trvale smazat uživatele \"{0}\"?", "MessageReportBugsAndContribute": "Hlásit chyby, žádat o funkce a přispívat", "MessageResetChaptersConfirm": "Opravdu chcete resetovat kapitoly a vrátit zpět provedené změny?", "MessageRestoreBackupConfirm": "Opravdu chcete obnovit zálohu vytvořenou dne?", "MessageRestoreBackupWarning": "Obnovení zálohy přepíše celou databázi umístěnou v /config a obálku obrázků v /metadata/items & /metadata/authors.<br /><br />Backups nezmění žádné soubory ve složkách knihovny. Pokud jste povolili nastavení serveru pro ukládání obrázků obalu a metadat do složek knihovny, nebudou zálohovány ani přepsány.<br /><br />Všichni klienti používající váš server budou automaticky obnoveni.", "MessageSearchResultsFor": "Výsledky hledání pro", - "MessageServerCouldNotBeReached": "Server nebyl dostupný", + "MessageServerCouldNotBeReached": "Server je nedostupný", "MessageSetChaptersFromTracksDescription": "Nastavit kapitoly jako kapitolu a název kapitoly jako název zvukového souboru", "MessageStartPlaybackAtTime": "Spustit přehrávání pro \"{0}\" v {1}?", "MessageThinking": "Přemýšlení...", "MessageUploaderItemFailed": "Nahrávání se nezdařilo", "MessageUploaderItemSuccess": "Nahráno bylo úspěšně!", "MessageUploading": "Odesílám...", - "MessageValidCronExpression": "Platný cron výraz", - "MessageWatcherIsDisabledGlobally": "Watcher je globálně zakázán v nastavení serveru", + "MessageValidCronExpression": "Platný výraz cronu", + "MessageWatcherIsDisabledGlobally": "Hlídač je globálně zakázán v nastavení serveru", "MessageXLibraryIsEmpty": "{0} knihovna je prázdná!", "MessageYourAudiobookDurationIsLonger": "Doba trvání audioknihy je delší než nalezená délka", "MessageYourAudiobookDurationIsShorter": "Délka audioknihy je kratší, než byla nalezena.", @@ -652,7 +652,7 @@ "NoteUploaderUnsupportedFiles": "Nepodporované soubory jsou ignorovány. Při výběru nebo přetažení složky jsou ostatní soubory, které nejsou ve složce položek, ignorovány.", "PlaceholderNewCollection": "Nový název kolekce", "PlaceholderNewFolderPath": "Nová cesta ke složce", - "PlaceholderNewPlaylist": "Nový název playlistu", + "PlaceholderNewPlaylist": "Nový název seznamu přehrávání", "PlaceholderSearch": "Hledat..", "PlaceholderSearchEpisode": "Hledat epizodu..", "ToastAccountUpdateFailed": "Aktualizace účtu se nezdařila", @@ -671,7 +671,7 @@ "ToastBackupUploadFailed": "Nepodařilo se nahrát zálohu", "ToastBackupUploadSuccess": "Záloha nahrána", "ToastBatchUpdateFailed": "Dávková aktualizace se nezdařila", - "ToastBatchUpdateSuccess": "Hromadná aktualizace proběhla úspěšně", + "ToastBatchUpdateSuccess": "Dávková aktualizace proběhla úspěšně", "ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo", "ToastBookmarkCreateSuccess": "Přidána záložka", "ToastBookmarkRemoveFailed": "Nepodařilo se odstranit záložku", @@ -686,8 +686,8 @@ "ToastCollectionRemoveSuccess": "Kolekce odstraněna", "ToastCollectionUpdateFailed": "Aktualizace kolekce se nezdařila", "ToastCollectionUpdateSuccess": "Kolekce aktualizována", - "ToastItemCoverUpdateFailed": "Aktualizace obalu položky se nezdařila", - "ToastItemCoverUpdateSuccess": "Obal předmětu byl aktualizován", + "ToastItemCoverUpdateFailed": "Aktualizace obálky se nezdařila", + "ToastItemCoverUpdateSuccess": "Obálka předmětu byl aktualizována", "ToastItemDetailsUpdateFailed": "Nepodařilo se aktualizovat podrobnosti o položce", "ToastItemDetailsUpdateSuccess": "Podrobnosti o položce byly aktualizovány", "ToastItemDetailsUpdateUnneeded": "Podrobnosti o položce nejsou potřeba aktualizovat", @@ -703,12 +703,12 @@ "ToastLibraryScanStarted": "Kontrola knihovny spuštěna", "ToastLibraryUpdateFailed": "Aktualizace knihovny se nezdařila", "ToastLibraryUpdateSuccess": "Knihovna \"{0}\" aktualizována", - "ToastPlaylistCreateFailed": "Vytvoření seznamu skladeb se nezdařilo", - "ToastPlaylistCreateSuccess": "Seznam skladeb vytvořen", - "ToastPlaylistRemoveFailed": "Nepodařilo se odstranit seznam skladeb", - "ToastPlaylistRemoveSuccess": "Seznam skladeb odstraněn", - "ToastPlaylistUpdateFailed": "Aktualizace seznamu skladeb se nezdařila", - "ToastPlaylistUpdateSuccess": "Seznam skladeb aktualizován", + "ToastPlaylistCreateFailed": "Vytvoření seznamu přehrávání se nezdařilo", + "ToastPlaylistCreateSuccess": "Seznam přehrávání vytvořen", + "ToastPlaylistRemoveFailed": "Nepodařilo se odstranit seznamu přehrávání", + "ToastPlaylistRemoveSuccess": "Seznam přehrávání odstraněn", + "ToastPlaylistUpdateFailed": "Aktualizace seznamu přehrávání se nezdařila", + "ToastPlaylistUpdateSuccess": "Seznam přehrávání aktualizován", "ToastPodcastCreateFailed": "Vytvoření podcastu se nezdařilo", "ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen", "ToastRemoveItemFromCollectionFailed": "Nepodařilo se odebrat položku z kolekce", @@ -717,13 +717,13 @@ "ToastRSSFeedCloseSuccess": "RSS kanál uzavřen", "ToastSendEbookToDeviceFailed": "Odeslání e-knihy do zařízení se nezdařilo", "ToastSendEbookToDeviceSuccess": "E-kniha odeslána do zařízení \"{0}\"", - "ToastSeriesUpdateFailed": "Aktualizace řady se nezdařila", + "ToastSeriesUpdateFailed": "Aktualizace série se nezdařila", "ToastSeriesUpdateSuccess": "Aktualizace série byla úspěšná", "ToastSessionDeleteFailed": "Nepodařilo se smazat relaci", "ToastSessionDeleteSuccess": "Relace smazána", - "ToastSocketConnected": "Zásuvka připojena", - "ToastSocketDisconnected": "Zásuvka odpojena", + "ToastSocketConnected": "Socket připojen", + "ToastSocketDisconnected": "Socket odpojen", "ToastSocketFailedToConnect": "Socket se nepodařilo připojit", "ToastUserDeleteFailed": "Nepodařilo se smazat uživatele", "ToastUserDeleteSuccess": "Uživatel smazán" -} \ No newline at end of file +} From 828b96b2d90eaabc5cb3574ce0b68cee9ec1944e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 2 Nov 2023 13:55:01 -0500 Subject: [PATCH 110/208] Add server settings for changing openid button text and auto launching openid --- client/pages/config/authentication.vue | 8 ++++++++ client/pages/login.vue | 19 +++++++++++++---- server/Server.js | 3 ++- server/objects/settings/ServerSettings.js | 25 +++++++++++++++++++---- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index acc0ac130..317364a2e 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -26,6 +26,14 @@ <ui-text-input-with-label ref="openidClientId" v-model="newAuthSettings.authOpenIDClientID" :disabled="savingSettings" :label="'Client ID'" class="mb-2" /> <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" /> + + <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="'Button Text'" class="mb-2" /> + + <div class="flex items-center py-2 px-1"> + <ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" /> + <p id="auto-redirect-toggle" class="pl-4">Auto Launch</p> + <p class="pl-4 text-sm text-gray-300">Redirect to the auth provider automatically when navigating to the /login page</p> + </div> </div> </transition> </div> diff --git a/client/pages/login.vue b/client/pages/login.vue index 4ca7c302f..724f5999f 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -48,7 +48,7 @@ <ui-btn color="primary" class="leading-none">Login with Google</ui-btn> </a> <a v-show="login_openid" :href="openidAuthUri"> - <ui-btn color="primary" class="leading-none">Login with OpenId</ui-btn> + <ui-btn color="primary" class="leading-none">{{ openIDButtonText }}</ui-btn> </a> </div> </div> @@ -77,7 +77,8 @@ export default { MetadataPath: '', login_local: true, login_google_oauth20: false, - login_openid: false + login_openid: false, + authFormData: null } }, watch: { @@ -116,6 +117,9 @@ export default { }, openidAuthUri() { return `${process.env.serverUrl}/auth/openid?callback=${location.toString()}` + }, + openIDButtonText() { + return this.authFormData?.authOpenIDButtonText || 'Login with OpenId' } }, methods: { @@ -221,7 +225,6 @@ export default { this.$axios .$get('/status') .then((data) => { - this.processing = false this.isInit = data.isInit this.showInitScreen = !data.isInit this.$setServerLanguageCode(data.language) @@ -229,14 +232,17 @@ export default { this.ConfigPath = data.ConfigPath || '' this.MetadataPath = data.MetadataPath || '' } else { + this.authFormData = data.authFormData this.updateLoginVisibility(data.authMethods || []) } }) .catch((error) => { console.error('Status check failed', error) - this.processing = false this.criticalError = 'Status check failed' }) + .finally(() => { + this.processing = false + }) }, updateLoginVisibility(authMethods) { if (authMethods.includes('local') || !authMethods.length) { @@ -252,6 +258,11 @@ export default { } if (authMethods.includes('openid')) { + // Auto redirect unless query string ?autoLaunch=0 + if (this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') { + window.location.href = this.openidAuthUri + } + this.login_openid = true } else { this.login_openid = false diff --git a/server/Server.js b/server/Server.js index 08f206dc8..6c6a17b08 100644 --- a/server/Server.js +++ b/server/Server.js @@ -230,7 +230,8 @@ class Server { const payload = { isInit: Database.hasRootUser, language: Database.serverSettings.language, - authMethods: Database.serverSettings.authActiveAuthMethods + authMethods: Database.serverSettings.authActiveAuthMethods, + authFormData: Database.serverSettings.authFormData } if (!payload.isInit) { payload.ConfigPath = global.ConfigPath diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 301e38e49..71e2e05d3 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -70,6 +70,8 @@ class ServerSettings { this.authOpenIDUserInfoURL = '' this.authOpenIDClientID = '' this.authOpenIDClientSecret = '' + this.authOpenIDButtonText = 'Login with OpenId' + this.authOpenIDAutoLaunch = false if (settings) { this.construct(settings) @@ -122,12 +124,14 @@ class ServerSettings { this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || '' this.authOpenIDClientID = settings.authOpenIDClientID || '' this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' + this.authOpenIDButtonText = settings.authOpenIDButtonText || 'Login with OpenId' + this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] } - // remove uninitialized methods + // remove uninitialized methods // GoogleOauth20 if (this.authActiveAuthMethods.includes('google-oauth20') && ( this.authGoogleOauth20ClientID === '' || @@ -137,7 +141,7 @@ class ServerSettings { this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1) } - // remove uninitialized methods + // remove uninitialized methods // OpenID if (this.authActiveAuthMethods.includes('openid') && ( this.authOpenIDIssuerURL === '' || @@ -221,7 +225,9 @@ class ServerSettings { authOpenIDTokenURL: this.authOpenIDTokenURL, authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client - authOpenIDClientSecret: this.authOpenIDClientSecret // Do not return to client + authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client + authOpenIDButtonText: this.authOpenIDButtonText, + authOpenIDAutoLaunch: this.authOpenIDAutoLaunch } } @@ -246,10 +252,21 @@ class ServerSettings { authOpenIDTokenURL: this.authOpenIDTokenURL, authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client - authOpenIDClientSecret: this.authOpenIDClientSecret // Do not return to client + authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client + authOpenIDButtonText: this.authOpenIDButtonText, + authOpenIDAutoLaunch: this.authOpenIDAutoLaunch } } + get authFormData() { + const clientFormData = {} + if (this.authActiveAuthMethods.includes('openid')) { + clientFormData.authOpenIDButtonText = this.authOpenIDButtonText + clientFormData.authOpenIDAutoLaunch = this.authOpenIDAutoLaunch + } + return clientFormData + } + /** * Update server settings * From 52203611513d2fe583183bf7abe1c328a5f273e8 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 3 Nov 2023 07:07:58 -0500 Subject: [PATCH 111/208] Fix:Podcast episode cron not adding/removing library items correctly #2277 --- server/managers/CronManager.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/managers/CronManager.js b/server/managers/CronManager.js index c44ad70d0..6d8f66665 100644 --- a/server/managers/CronManager.js +++ b/server/managers/CronManager.js @@ -127,8 +127,7 @@ class CronManager { } } - async executePodcastCron(expression, libraryItemIds) { - Logger.debug(`[CronManager] Start executing podcast cron ${expression} for ${libraryItemIds.length} item(s)`) + async executePodcastCron(expression) { const podcastCron = this.podcastCrons.find(cron => cron.expression === expression) if (!podcastCron) { Logger.error(`[CronManager] Podcast cron not found for expression ${expression}`) @@ -136,6 +135,9 @@ class CronManager { } this.podcastCronExpressionsExecuting.push(expression) + const libraryItemIds = podcastCron.libraryItemIds + Logger.debug(`[CronManager] Start executing podcast cron ${expression} for ${libraryItemIds.length} item(s)`) + // Get podcast library items to check const libraryItems = [] for (const libraryItemId of libraryItemIds) { From 567e1c46db5e8e2c081b541d2649da29a004dd88 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sat, 4 Nov 2023 11:06:54 +0000 Subject: [PATCH 112/208] Fix handling of single mefia file updates --- server/scanner/LibraryScanner.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index 11a88bd4b..27c507bd8 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -463,7 +463,7 @@ class LibraryScanner { // Test Case: Moving audio files from library item folder to author folder should trigger a re-scan of the item const updateGroup = { ...fileUpdateGroup } for (const itemDir in updateGroup) { - if (itemDir == fileUpdateGroup[itemDir]) continue // Media in root path + if (isSingleMediaFile(fileUpdateGroup, itemDir)) continue // Media in root path const itemDirNestedFiles = fileUpdateGroup[itemDir].filter(b => b.includes('/')) if (!itemDirNestedFiles.length) continue @@ -559,7 +559,7 @@ class LibraryScanner { Logger.debug(`[LibraryScanner] Folder update for relative path "${itemDir}" is in library item "${existingLibraryItem.media.metadata.title}" - scan for updates`) itemGroupingResults[itemDir] = await LibraryItemScanner.scanLibraryItem(existingLibraryItem.id, renamedPaths) continue - } else if (library.settings.audiobooksOnly && !fileUpdateGroup[itemDir].some?.(scanUtils.checkFilepathIsAudioFile)) { + } else if (library.settings.audiobooksOnly && !hasAudioFiles(fileUpdateGroup, itemDir)) { Logger.debug(`[LibraryScanner] Folder update for relative path "${itemDir}" has no audio files`) continue } @@ -580,7 +580,7 @@ class LibraryScanner { } Logger.debug(`[LibraryScanner] Folder update group must be a new item "${itemDir}" in library "${library.name}"`) - const isSingleMediaItem = itemDir === fileUpdateGroup[itemDir] + const isSingleMediaItem = isSingleMediaFile(fileUpdateGroup, itemDir) const newLibraryItem = await LibraryItemScanner.scanPotentialNewLibraryItem(fullPath, library, folder, isSingleMediaItem) if (newLibraryItem) { const oldNewLibraryItem = Database.libraryItemModel.getOldLibraryItem(newLibraryItem) @@ -592,4 +592,14 @@ class LibraryScanner { return itemGroupingResults } } -module.exports = new LibraryScanner() \ No newline at end of file +module.exports = new LibraryScanner() + +function hasAudioFiles(fileUpdateGroup, itemDir) { + return isSingleMediaFile(fileUpdateGroup, itemDir) ? + scanUtils.checkFilepathIsAudioFile(fileUpdateGroup[itemDir]) : + fileUpdateGroup[itemDir].some(scanUtils.checkFilepathIsAudioFile) +} + +function isSingleMediaFile(fileUpdateGroup, itemDir) { + return itemDir === fileUpdateGroup[itemDir] +} From 840811b46460d08690dd59c24dcc24daafb2587f Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 4 Nov 2023 15:36:43 -0500 Subject: [PATCH 113/208] Replace passport openidconnect plugin with openid-client, add JWKS and logout URL server settings, use email and email_verified instead of username --- client/pages/config/authentication.vue | 8 ++ package-lock.json | 91 ++++++++++++++++------- package.json | 2 +- server/Auth.js | 59 ++++++++++----- server/objects/settings/ServerSettings.js | 9 +++ 5 files changed, 125 insertions(+), 44 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 317364a2e..13867ef3a 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -23,6 +23,10 @@ <ui-text-input-with-label ref="userInfoUrl" v-model="newAuthSettings.authOpenIDUserInfoURL" :disabled="savingSettings" :label="'Userinfo URL'" class="mb-2" /> + <ui-text-input-with-label ref="jwksUrl" v-model="newAuthSettings.authOpenIDJwksURL" :disabled="savingSettings" :label="'JWKS URL'" class="mb-2" /> + + <ui-text-input-with-label ref="logoutUrl" v-model="newAuthSettings.authOpenIDLogoutURL" :disabled="savingSettings" :label="'Logout URL'" class="mb-2" /> + <ui-text-input-with-label ref="openidClientId" v-model="newAuthSettings.authOpenIDClientID" :disabled="savingSettings" :label="'Client ID'" class="mb-2" /> <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" /> @@ -97,6 +101,10 @@ export default { this.$toast.error('Userinfo URL required') isValid = false } + if (!this.newAuthSettings.authOpenIDJwksURL) { + this.$toast.error('JWKS URL required') + isValid = false + } if (!this.newAuthSettings.authOpenIDClientID) { this.$toast.error('Client ID required') isValid = false diff --git a/package-lock.json b/package-lock.json index 55149176c..dd2b8339b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,10 @@ "htmlparser2": "^8.0.1", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", + "openid-client": "^5.6.1", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", - "passport-openidconnect": "^0.1.1", "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", @@ -1343,6 +1343,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "node_modules/jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", @@ -1883,6 +1891,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -1891,6 +1907,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1918,6 +1942,20 @@ "wrappy": "1" } }, + "node_modules/openid-client": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", + "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", + "dependencies": { + "jose": "^4.15.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -1997,22 +2035,6 @@ "url": "https://github.com/sponsors/jaredhanson" } }, - "node_modules/passport-openidconnect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", - "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", - "dependencies": { - "oauth": "0.9.x", - "passport-strategy": "1.x.x" - }, - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -3929,6 +3951,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==" + }, "jsonwebtoken": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", @@ -4330,11 +4357,21 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + }, "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" }, + "oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==" + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4356,6 +4393,17 @@ "wrappy": "1" } }, + "openid-client": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", + "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", + "requires": { + "jose": "^4.15.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + } + }, "p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -4409,15 +4457,6 @@ "utils-merge": "1.x.x" } }, - "passport-openidconnect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", - "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", - "requires": { - "oauth": "0.9.x", - "passport-strategy": "1.x.x" - } - }, "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", diff --git a/package.json b/package.json index a3b4a7c59..d4e9c209f 100644 --- a/package.json +++ b/package.json @@ -39,10 +39,10 @@ "htmlparser2": "^8.0.1", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", + "openid-client": "^5.6.1", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", - "passport-openidconnect": "^0.1.1", "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", diff --git a/server/Auth.js b/server/Auth.js index b7ea59c4f..fa5020a0c 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -5,8 +5,9 @@ const LocalStrategy = require('./libs/passportLocal') const JwtStrategy = require('passport-jwt').Strategy const ExtractJwt = require('passport-jwt').ExtractJwt const GoogleStrategy = require('passport-google-oauth20').Strategy -const OpenIDConnectStrategy = require('passport-openidconnect') +const OpenIDClient = require('openid-client') const Database = require('./Database') +const Logger = require('./Logger') /** * @class Class for handling all the authentication related functionality. @@ -62,20 +63,33 @@ class Auth { // Check if we should load the openid strategy if (global.ServerSettings.authActiveAuthMethods.includes("openid")) { - passport.use(new OpenIDConnectStrategy({ + const openIdIssuerClient = new OpenIDClient.Issuer({ issuer: global.ServerSettings.authOpenIDIssuerURL, - authorizationURL: global.ServerSettings.authOpenIDAuthorizationURL, - tokenURL: global.ServerSettings.authOpenIDTokenURL, - userInfoURL: global.ServerSettings.authOpenIDUserInfoURL, - clientID: global.ServerSettings.authOpenIDClientID, - clientSecret: global.ServerSettings.authOpenIDClientSecret, - callbackURL: '/auth/openid/callback', - scope: ["openid", "email", "profile"], - skipUserProfile: false - }, async (issuer, profile, done) => { - // TODO: do we want to create the users which does not exist? + authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL, + token_endpoint: global.ServerSettings.authOpenIDTokenURL, + userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL, + jwks_uri: global.ServerSettings.authOpenIDJwksURL + }).Client + const openIdClient = new openIdIssuerClient({ + client_id: global.ServerSettings.authOpenIDClientID, + client_secret: global.ServerSettings.authOpenIDClientSecret + }) + const openIdClientStrategy = new OpenIDClient.Strategy({ + client: openIdClient, + params: { + redirect_uri: '/auth/openid/callback', + scope: 'openid profile email' + } + }, async (tokenset, userinfo, done) => { + // TODO: Here is where to lookup the Abs user or register a new Abs user + Logger.debug(`[Auth] openid callback userinfo=`, userinfo) - const user = await Database.userModel.getUserByUsername(profile.username) + let user = null + // TODO: Temporary lookup existing user by email. May be replaced by a setting to toggle this or use name + if (userinfo.email && userinfo.email_verified) { + user = await Database.userModel.getUserByEmail(userinfo.email) + // TODO: If using existing user then save userinfo.sub on user + } if (!user?.isActive) { // deny login @@ -85,7 +99,12 @@ class Auth { // permit login return done(null, user) - })) + }) + // The strategy name is set to the issuer hostname by default but didnt' see a way to override this + // @see https://github.com/panva/node-openid-client/blob/a84d022f195f82ca1c97f8f6b2567ebcef8738c3/lib/passport_strategy.js#L75 + openIdClientStrategy.name = 'openid-client' + + passport.use(openIdClientStrategy) } // Load the JwtStrategy (always) -> for bearer token auth @@ -99,7 +118,7 @@ class Auth { process.nextTick(function () { // only store id to session return cb(null, JSON.stringify({ - "id": user.id, + id: user.id, })) }) }) @@ -216,7 +235,13 @@ class Auth { // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { - const auth_func = passport.authenticate('openidconnect') + // This is a (temporary?) hack to not have to get the full redirect URL from the user + // it uses the URL made in this request and adds the relative URL /auth/openid/callback + const strategy = passport._strategy('openid-client') + strategy._params.redirect_uri = new URL(`${req.protocol}://${req.get('host')}/auth/openid/callback`).toString() + + + const auth_func = passport.authenticate('openid-client') // params (isRest, callback) to a cookie that will be send to the client this.paramsToCookies(req, res) auth_func(req, res, next) @@ -224,7 +249,7 @@ class Auth { // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', - passport.authenticate('openidconnect'), + passport.authenticate('openid-client'), // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this)) diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 71e2e05d3..781943b49 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -68,6 +68,8 @@ class ServerSettings { this.authOpenIDAuthorizationURL = '' this.authOpenIDTokenURL = '' this.authOpenIDUserInfoURL = '' + this.authOpenIDJwksURL = '' + this.authOpenIDLogoutURL = '' this.authOpenIDClientID = '' this.authOpenIDClientSecret = '' this.authOpenIDButtonText = 'Login with OpenId' @@ -122,6 +124,8 @@ class ServerSettings { this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || '' this.authOpenIDTokenURL = settings.authOpenIDTokenURL || '' this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || '' + this.authOpenIDJwksURL = settings.authOpenIDJwksURL || '' + this.authOpenIDLogoutURL = settings.authOpenIDLogoutURL || '' this.authOpenIDClientID = settings.authOpenIDClientID || '' this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' this.authOpenIDButtonText = settings.authOpenIDButtonText || 'Login with OpenId' @@ -148,6 +152,7 @@ class ServerSettings { this.authOpenIDAuthorizationURL === '' || this.authOpenIDTokenURL === '' || this.authOpenIDUserInfoURL === '' || + this.authOpenIDJwksURL === '' || this.authOpenIDClientID === '' || this.authOpenIDClientSecret === '' )) { @@ -224,6 +229,8 @@ class ServerSettings { authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, authOpenIDTokenURL: this.authOpenIDTokenURL, authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, + authOpenIDJwksURL: this.authOpenIDJwksURL, + authOpenIDLogoutURL: this.authOpenIDLogoutURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client authOpenIDButtonText: this.authOpenIDButtonText, @@ -251,6 +258,8 @@ class ServerSettings { authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, authOpenIDTokenURL: this.authOpenIDTokenURL, authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, + authOpenIDJwksURL: this.authOpenIDJwksURL, + authOpenIDLogoutURL: this.authOpenIDLogoutURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client authOpenIDButtonText: this.authOpenIDButtonText, From 8f5a6b7c959f2a08880b7dbb7822c008939260e4 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sun, 5 Nov 2023 06:41:23 +0000 Subject: [PATCH 114/208] Move utility functions to module scope --- server/finders/BookFinder.js | 105 +++++++++++++++++------------------ 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 75e5a5f11..1af3c0a3d 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -31,52 +31,11 @@ class BookFinder { return book } - stripSubtitle(title) { - if (title.includes(':')) { - return title.split(':')[0].trim() - } else if (title.includes(' - ')) { - return title.split(' - ')[0].trim() - } - return title - } - - replaceAccentedChars(str) { - try { - return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "") - } catch (error) { - Logger.error('[BookFinder] str normalize error', error) - return str - } - } - - cleanTitleForCompares(title) { - if (!title) return '' - // Remove subtitle if there (i.e. "Cool Book: Coolest Ever" becomes "Cool Book") - let stripped = this.stripSubtitle(title) - - // Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game") - let cleaned = stripped.replace(/ *\([^)]*\) */g, "") - - // Remove single quotes (i.e. "Ender's Game" becomes "Enders Game") - cleaned = cleaned.replace(/'/g, '') - return this.replaceAccentedChars(cleaned).toLowerCase() - } - - cleanAuthorForCompares(author) { - if (!author) return '' - let cleanAuthor = this.replaceAccentedChars(author).toLowerCase() - // separate initials - cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2') - // remove middle initials - cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '') - return cleanAuthor - } - filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) { - var searchTitle = this.cleanTitleForCompares(title) - var searchAuthor = this.cleanAuthorForCompares(author) + var searchTitle = cleanTitleForCompares(title) + var searchAuthor = cleanAuthorForCompares(author) return books.map(b => { - b.cleanedTitle = this.cleanTitleForCompares(b.title) + b.cleanedTitle = cleanTitleForCompares(b.title) b.titleDistance = levenshteinDistance(b.cleanedTitle, title) // Total length of search (title or both title & author) @@ -87,7 +46,7 @@ class BookFinder { b.authorDistance = author.length } else { b.totalPossibleDistance += b.author.length - b.cleanedAuthor = this.cleanAuthorForCompares(b.author) + b.cleanedAuthor = cleanAuthorForCompares(b.author) var cleanedAuthorDistance = levenshteinDistance(b.cleanedAuthor, searchAuthor) var authorDistance = levenshteinDistance(b.author || '', author) @@ -190,8 +149,7 @@ class BookFinder { static TitleCandidates = class { - constructor(bookFinder, cleanAuthor) { - this.bookFinder = bookFinder + constructor(cleanAuthor) { this.candidates = new Set() this.cleanAuthor = cleanAuthor this.priorities = {} @@ -202,7 +160,7 @@ class BookFinder { // if title contains the author, remove it if (this.cleanAuthor) { const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g") - title = this.bookFinder.cleanAuthorForCompares(title).replace(authorRe, '').trim() + title = cleanAuthorForCompares(title).replace(authorRe, '').trim() } const titleTransformers = [ @@ -215,7 +173,7 @@ class BookFinder { ] // Main variant - const cleanTitle = this.bookFinder.cleanTitleForCompares(title).trim() + const cleanTitle = cleanTitleForCompares(title).trim() if (!cleanTitle) return this.candidates.add(cleanTitle) this.priorities[cleanTitle] = 0 @@ -283,7 +241,7 @@ class BookFinder { return this.bookFinder.audnexus.authorASINsRequest(name, region).then((asins) => { for (const [i, asin] of asins.entries()) { if (i > 10) break - let cleanName = this.bookFinder.cleanAuthorForCompares(asin.name) + let cleanName = cleanAuthorForCompares(asin.name) if (!cleanName) continue if (cleanName.includes(name)) return name if (name.includes(cleanName)) return cleanName @@ -294,7 +252,7 @@ class BookFinder { } add(author) { - const cleanAuthor = this.bookFinder.cleanAuthorForCompares(author).trim() + const cleanAuthor = cleanAuthorForCompares(author).trim() if (!cleanAuthor) return this.candidates.add(cleanAuthor) } @@ -362,7 +320,7 @@ class BookFinder { title = title.trim().toLowerCase() author = author?.trim().toLowerCase() || '' - const cleanAuthor = this.cleanAuthorForCompares(author) + const cleanAuthor = cleanAuthorForCompares(author) // Now run up to maxFuzzySearches fuzzy searches let authorCandidates = new BookFinder.AuthorCandidates(this, cleanAuthor) @@ -375,7 +333,7 @@ class BookFinder { authorCandidates.add(titlePart) authorCandidates = await authorCandidates.getCandidates() for (const authorCandidate of authorCandidates) { - let titleCandidates = new BookFinder.TitleCandidates(this, authorCandidate) + let titleCandidates = new BookFinder.TitleCandidates(authorCandidate) for (const [position, titlePart] of titleParts.entries()) titleCandidates.add(titlePart, position) titleCandidates = titleCandidates.getCandidates() @@ -457,3 +415,44 @@ class BookFinder { } } module.exports = new BookFinder() + +function stripSubtitle(title) { + if (title.includes(':')) { + return title.split(':')[0].trim() + } else if (title.includes(' - ')) { + return title.split(' - ')[0].trim() + } + return title +} + +function replaceAccentedChars(str) { + try { + return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "") + } catch (error) { + Logger.error('[BookFinder] str normalize error', error) + return str + } +} + +function cleanTitleForCompares(title) { + if (!title) return '' + // Remove subtitle if there (i.e. "Cool Book: Coolest Ever" becomes "Cool Book") + let stripped = stripSubtitle(title) + + // Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game") + let cleaned = stripped.replace(/ *\([^)]*\) */g, "") + + // Remove single quotes (i.e. "Ender's Game" becomes "Enders Game") + cleaned = cleaned.replace(/'/g, '') + return replaceAccentedChars(cleaned).toLowerCase() +} + +function cleanAuthorForCompares(author) { + if (!author) return '' + let cleanAuthor = replaceAccentedChars(author).toLowerCase() + // separate initials + cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2') + // remove middle initials + cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '') + return cleanAuthor +} From ee3d3808ef49ecfdd08f8725c57fbe63c52d42ab Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sun, 5 Nov 2023 14:31:36 +0000 Subject: [PATCH 115/208] Refactor removing author from title candidate --- server/finders/BookFinder.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 1af3c0a3d..f5f150d2f 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -158,10 +158,7 @@ class BookFinder { add(title, position = 0) { // if title contains the author, remove it - if (this.cleanAuthor) { - const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g") - title = cleanAuthorForCompares(title).replace(authorRe, '').trim() - } + title = this.#removeAuthorFromTitle(title) const titleTransformers = [ [/([,:;_]| by ).*/g, ''], // Remove subtitle @@ -227,6 +224,17 @@ class BookFinder { delete(title) { return this.candidates.delete(title) } + + #removeAuthorFromTitle(title) { + if (!this.cleanAuthor) return title + const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g") + const authorCleanedTitle = cleanAuthorForCompares(title) + const authorCleanedTitleWithoutAuthor = authorCleanedTitle.replace(authorRe, '') + if (authorCleanedTitleWithoutAuthor !== authorCleanedTitle) { + return authorCleanedTitleWithoutAuthor.trim() + } + return title + } } static AuthorCandidates = class { From 3a9d09ea637ac3a098c1b34fc447d56d8391201c Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sun, 5 Nov 2023 14:33:56 +0000 Subject: [PATCH 116/208] Add jest to dev dependencies --- package-lock.json | 5784 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- 2 files changed, 5774 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 888c3beb4..650850c6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,15 +25,1039 @@ "audiobookshelf": "prod.js" }, "devDependencies": { + "jest": "^29.7.0", "nodemon": "^2.0.20" } }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", + "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", + "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -120,6 +1144,30 @@ "node": ">=10" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -134,6 +1182,47 @@ "node": ">= 6" } }, + "node_modules/@types/babel__core": { + "version": "7.20.3", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", + "integrity": "sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.6.tgz", + "integrity": "sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.3.tgz", + "integrity": "sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.3.tgz", + "integrity": "sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, "node_modules/@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", @@ -155,6 +1244,39 @@ "@types/ms": "*" } }, + "node_modules/@types/graceful-fs": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.8.tgz", + "integrity": "sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz", + "integrity": "sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz", + "integrity": "sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, "node_modules/@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -165,11 +1287,32 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "node_modules/@types/stack-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz", + "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", + "dev": true + }, "node_modules/@types/validator": { "version": "13.7.17", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz", "integrity": "sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==" }, + "node_modules/@types/yargs": { + "version": "17.0.29", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", + "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", + "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==", + "dev": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -269,6 +1412,21 @@ "node": ">=8" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -277,6 +1435,21 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -307,6 +1480,15 @@ "node": ">=10" } }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -326,6 +1508,122 @@ "form-data": "^4.0.0" } }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -392,6 +1690,53 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -441,6 +1786,90 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -476,6 +1905,27 @@ "node": ">=10" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -485,6 +1935,54 @@ "node": ">=6" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -533,6 +2031,12 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -558,6 +2062,41 @@ "node": ">= 0.10" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -566,6 +2105,29 @@ "ms": "2.0.0" } }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -604,6 +2166,24 @@ "node": ">=8" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -665,6 +2245,24 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/electron-to-chromium": { + "version": "1.4.576", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz", + "integrity": "sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -782,11 +2380,51 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "optional": true }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "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==" }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -795,6 +2433,54 @@ "node": ">= 0.6" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -836,6 +2522,21 @@ "node": ">= 0.10.0" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -865,6 +2566,19 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -944,9 +2658,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/gauge": { "version": "3.0.2", @@ -967,6 +2684,24 @@ "node": ">=10" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -980,6 +2715,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1011,6 +2767,15 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -1052,6 +2817,24 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/htmlparser2": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", @@ -1161,6 +2944,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -1187,11 +2979,30 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "optional": true, + "devOptional": true, "engines": { "node": ">=0.8.19" } @@ -1247,6 +3058,12 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1259,6 +3076,18 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1276,6 +3105,15 @@ "node": ">=8" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1303,11 +3141,842 @@ "node": ">=0.12.0" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true + "devOptional": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/lodash": { "version": "4.17.21", @@ -1374,6 +4043,15 @@ "node": ">= 10" } }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1387,6 +4065,12 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -1395,6 +4079,19 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -1425,6 +4122,15 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1559,6 +4265,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1692,6 +4404,18 @@ "node": ">=10" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, "node_modules/node-tone": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz", @@ -1772,6 +4496,18 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -1818,6 +4554,63 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -1833,6 +4626,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1841,6 +4661,15 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1849,6 +4678,21 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -1859,6 +4703,12 @@ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1871,6 +4721,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -1890,6 +4787,19 @@ "node": ">=10" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1908,6 +4818,22 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -1944,6 +4870,12 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -1969,6 +4901,62 @@ "node": ">=8.10.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -2191,6 +5179,27 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -2230,6 +5239,21 @@ "semver": "bin/semver.js" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -2366,6 +5390,31 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/sqlite3": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", @@ -2416,6 +5465,18 @@ "node": ">= 8" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -2432,6 +5493,19 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2456,6 +5530,36 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2468,6 +5572,18 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tar": { "version": "6.1.15", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", @@ -2492,6 +5608,35 @@ "node": ">=8" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2534,6 +5679,27 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2578,6 +5744,36 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2599,6 +5795,20 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", + "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/validator": { "version": "13.9.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", @@ -2615,6 +5825,15 @@ "node": ">= 0.8" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -2633,7 +5852,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "optional": true, + "devOptional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -2660,11 +5879,41 @@ "@types/node": "*" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "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==" }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/ws": { "version": "8.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", @@ -2705,19 +5954,857 @@ "node": ">=4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } }, "dependencies": { + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + } + } + }, + "@babel/compat-data": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", + "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", + "dev": true + }, + "@babel/core": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", + "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true + }, + "@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + } + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + } + } + }, + "@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -2783,6 +6870,30 @@ "rimraf": "^3.0.2" } }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -2794,6 +6905,47 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "optional": true }, + "@types/babel__core": { + "version": "7.20.3", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", + "integrity": "sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.6.tgz", + "integrity": "sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.3.tgz", + "integrity": "sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.3.tgz", + "integrity": "sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, "@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", @@ -2815,6 +6967,39 @@ "@types/ms": "*" } }, + "@types/graceful-fs": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.8.tgz", + "integrity": "sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz", + "integrity": "sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz", + "integrity": "sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -2825,11 +7010,32 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "@types/stack-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz", + "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", + "dev": true + }, "@types/validator": { "version": "13.7.17", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz", "integrity": "sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==" }, + "@types/yargs": { + "version": "17.0.29", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", + "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", + "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2905,11 +7111,29 @@ "indent-string": "^4.0.0" } }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2934,6 +7158,15 @@ "readable-stream": "^3.6.0" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -2953,6 +7186,97 @@ "form-data": "^4.0.0" } }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3006,6 +7330,33 @@ "fill-range": "^7.0.1" } }, + "browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3046,6 +7397,57 @@ "get-intrinsic": "^1.0.2" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -3067,12 +7469,62 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" }, + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "optional": true }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -3109,6 +7561,12 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -3128,6 +7586,32 @@ "vary": "^1" } }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3136,6 +7620,19 @@ "ms": "2.0.0" } }, + "dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "requires": {} + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3161,6 +7658,18 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, "dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -3204,6 +7713,18 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "electron-to-chromium": { + "version": "1.4.576", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz", + "integrity": "sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3293,16 +7814,79 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "optional": true }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "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==" }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, "express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -3341,6 +7925,21 @@ "vary": "~1.1.2" } }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3364,6 +7963,16 @@ "unpipe": "~1.0.0" } }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -3410,9 +8019,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "gauge": { "version": "3.0.2", @@ -3430,6 +8039,18 @@ "wide-align": "^1.1.2" } }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, "get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -3440,6 +8061,18 @@ "has-symbols": "^1.0.3" } }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3462,6 +8095,12 @@ "is-glob": "^4.0.1" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -3491,6 +8130,21 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "htmlparser2": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", @@ -3572,6 +8226,12 @@ } } }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, "humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -3595,11 +8255,21 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "optional": true + "devOptional": true }, "indent-string": { "version": "4.0.0", @@ -3643,6 +8313,12 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3652,6 +8328,15 @@ "binary-extensions": "^2.0.0" } }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3663,6 +8348,12 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3684,11 +8375,635 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true + "devOptional": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } }, "lodash": { "version": "4.17.21", @@ -3742,6 +9057,15 @@ "ssri": "^8.0.0" } }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -3752,11 +9076,27 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -3775,6 +9115,12 @@ "mime-db": "1.52.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3871,6 +9217,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -3965,6 +9317,18 @@ } } }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, "node-tone": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz", @@ -4025,6 +9389,15 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -4062,6 +9435,44 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, "p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -4071,16 +9482,52 @@ "aggregate-error": "^3.0.0" } }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -4091,12 +9538,52 @@ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -4113,6 +9600,16 @@ "retry": "^0.12.0" } }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4128,6 +9625,12 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true + }, "qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -4152,6 +9655,12 @@ "unpipe": "1.0.0" } }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -4171,6 +9680,44 @@ "picomatch": "^2.2.1" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -4310,6 +9857,21 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -4342,6 +9904,18 @@ } } }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -4443,6 +10017,28 @@ } } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "sqlite3": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", @@ -4478,6 +10074,15 @@ "minipass": "^3.1.1" } }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + } + }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -4491,6 +10096,16 @@ "safe-buffer": "~5.2.0" } }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4509,6 +10124,24 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4518,6 +10151,12 @@ "has-flag": "^3.0.0" } }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, "tar": { "version": "6.1.15", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", @@ -4538,6 +10177,29 @@ } } }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4571,6 +10233,18 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -4609,6 +10283,16 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4624,6 +10308,17 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, + "v8-to-istanbul": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", + "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, "validator": { "version": "13.9.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", @@ -4634,6 +10329,15 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -4652,7 +10356,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "optional": true, + "devOptional": true, "requires": { "isexe": "^2.0.0" } @@ -4673,11 +10377,32 @@ "@types/node": "*" } }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, "ws": { "version": "8.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", @@ -4698,10 +10423,43 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 4bef0e425..c21a12f6c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local", "docker-arm64-local": "docker buildx build --platform linux/arm64 --load . -t advplyr/audiobookshelf-arm64-local", "docker-armv7-local": "docker buildx build --platform linux/arm/v7 --load . -t advplyr/audiobookshelf-armv7-local", - "deploy-linux": "node deploy/linux" + "deploy-linux": "node deploy/linux", + "test": "jest" }, "bin": "prod.js", "pkg": { @@ -44,6 +45,7 @@ "xml2js": "^0.5.0" }, "devDependencies": { + "jest": "^29.7.0", "nodemon": "^2.0.20" } } \ No newline at end of file From 047e7a72f21d9023b80e32bc0f63c6792acdd6f4 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sun, 5 Nov 2023 14:56:20 +0000 Subject: [PATCH 117/208] Make position an internal property of titleCandidates --- server/finders/BookFinder.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index f5f150d2f..212c588a1 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -154,9 +154,10 @@ class BookFinder { this.cleanAuthor = cleanAuthor this.priorities = {} this.positions = {} + this.currentPosition = 0 } - add(title, position = 0) { + add(title) { // if title contains the author, remove it title = this.#removeAuthorFromTitle(title) @@ -174,7 +175,7 @@ class BookFinder { if (!cleanTitle) return this.candidates.add(cleanTitle) this.priorities[cleanTitle] = 0 - this.positions[cleanTitle] = position + this.positions[cleanTitle] = this.currentPosition let candidate = cleanTitle @@ -185,10 +186,11 @@ class BookFinder { if (candidate) { this.candidates.add(candidate) this.priorities[candidate] = 0 - this.positions[candidate] = position + this.positions[candidate] = this.currentPosition } this.priorities[cleanTitle] = 1 } + this.currentPosition++ } get size() { @@ -210,11 +212,7 @@ class BookFinder { if (priorityDiff) return priorityDiff // if same priorirty, prefer candidates that are closer to the beginning (e.g. titles before subtitles) const positionDiff = this.positions[a] - this.positions[b] - if (positionDiff) return positionDiff - // Start with longer candidaets, as they are likely more specific - const lengthDiff = b.length - a.length - if (lengthDiff) return lengthDiff - return b.localeCompare(a) + return positionDiff // candidates with same priority always have different positions }) Logger.debug(`[${this.constructor.name}] Found ${candidates.length} fuzzy title candidates`) Logger.debug(candidates) @@ -342,8 +340,8 @@ class BookFinder { authorCandidates = await authorCandidates.getCandidates() for (const authorCandidate of authorCandidates) { let titleCandidates = new BookFinder.TitleCandidates(authorCandidate) - for (const [position, titlePart] of titleParts.entries()) - titleCandidates.add(titlePart, position) + for (const titlePart of titleParts) + titleCandidates.add(titlePart) titleCandidates = titleCandidates.getCandidates() for (const titleCandidate of titleCandidates) { if (titleCandidate == title && authorCandidate == author) continue // We already tried this From 5a3d450482b31c31f0d60c049f499fb9d871c829 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sun, 5 Nov 2023 15:13:42 +0000 Subject: [PATCH 118/208] Refactor diff declarations in title candidate sorting --- server/finders/BookFinder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 212c588a1..e3a84be09 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -201,11 +201,11 @@ class BookFinder { var candidates = [...this.candidates] candidates.sort((a, b) => { // Candidates that include the author are likely low quality - const includesAuthorDiff = !b.includes(this.cleanAuthor) - !a.includes(this.cleanAuthor) + const includesAuthorDiff = a.includes(this.cleanAuthor) - b.includes(this.cleanAuthor) if (includesAuthorDiff) return includesAuthorDiff // Candidates that include only digits are also likely low quality const onlyDigits = /^\d+$/ - const includesOnlyDigitsDiff = !onlyDigits.test(b) - !onlyDigits.test(a) + const includesOnlyDigitsDiff = onlyDigits.test(a) - onlyDigits.test(b) if (includesOnlyDigitsDiff) return includesOnlyDigitsDiff // transformed candidates receive higher priority const priorityDiff = this.priorities[a] - this.priorities[b] From b9ccc28baa07be3dd21fb89cb280c0a4f180a662 Mon Sep 17 00:00:00 2001 From: Gustav Almstrom <gustav@almstrom.org> Date: Sun, 5 Nov 2023 16:51:45 +0100 Subject: [PATCH 119/208] Added swedish translation of strings --- client/strings/se.json | 729 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 729 insertions(+) create mode 100644 client/strings/se.json diff --git a/client/strings/se.json b/client/strings/se.json new file mode 100644 index 000000000..f0580847c --- /dev/null +++ b/client/strings/se.json @@ -0,0 +1,729 @@ +{ + "ButtonAdd": "Lägg till", + "ButtonAddChapters": "Lägg till kapitel", + "ButtonAddDevice": "Lägg till enhet", + "ButtonAddLibrary": "Lägg till bibliotek", + "ButtonAddPodcasts": "Lägg till podcasts", + "ButtonAddUser": "Lägg till användare", + "ButtonAddYourFirstLibrary": "Lägg till ditt första bibliotek", + "ButtonApply": "Tillämpa", + "ButtonApplyChapters": "Tillämpa kapitel", + "ButtonAuthors": "Författare", + "ButtonBrowseForFolder": "Bläddra efter mapp", + "ButtonCancel": "Avbryt", + "ButtonCancelEncode": "Avbryt kodning", + "ButtonChangeRootPassword": "Ändra rootlösenord", + "ButtonCheckAndDownloadNewEpisodes": "Kontrollera och ladda ner nya avsnitt", + "ButtonChooseAFolder": "Välj en mapp", + "ButtonChooseFiles": "Välj filer", + "ButtonClearFilter": "Rensa filter", + "ButtonCloseFeed": "Stäng flöde", + "ButtonCollections": "Samlingar", + "ButtonConfigureScanner": "Konfigurera skanner", + "ButtonCreate": "Skapa", + "ButtonCreateBackup": "Skapa säkerhetskopia", + "ButtonDelete": "Radera", + "ButtonDownloadQueue": "Kö", + "ButtonEdit": "Redigera", + "ButtonEditChapters": "Redigera kapitel", + "ButtonEditPodcast": "Redigera podcast", + "ButtonForceReScan": "Tvinga omstart", + "ButtonFullPath": "Full sökväg", + "ButtonHide": "Dölj", + "ButtonHome": "Hem", + "ButtonIssues": "Problem", + "ButtonLatest": "Senaste", + "ButtonLibrary": "Bibliotek", + "ButtonLogout": "Logga ut", + "ButtonLookup": "Sök", + "ButtonManageTracks": "Hantera spår", + "ButtonMapChapterTitles": "Karta kapitelrubriker", + "ButtonMatchAllAuthors": "Matcha alla författare", + "ButtonMatchBooks": "Matcha böcker", + "ButtonNevermind": "Glöm det", + "ButtonOk": "Okej", + "ButtonOpenFeed": "Öppna flöde", + "ButtonOpenManager": "Öppna Manager", + "ButtonPlay": "Spela", + "ButtonPlaying": "Spelar", + "ButtonPlaylists": "Spellistor", + "ButtonPurgeAllCache": "Rensa all cache", + "ButtonPurgeItemsCache": "Rensa föremåls-cache", + "ButtonPurgeMediaProgress": "Rensa medieförlopp", + "ButtonQueueAddItem": "Lägg till i kön", + "ButtonQueueRemoveItem": "Ta bort från kön", + "ButtonQuickMatch": "Snabb matchning", + "ButtonRead": "Läs", + "ButtonRemove": "Ta bort", + "ButtonRemoveAll": "Ta bort alla", + "ButtonRemoveAllLibraryItems": "Ta bort alla biblioteksobjekt", + "ButtonRemoveFromContinueListening": "Ta bort från Fortsätt lyssna", + "ButtonRemoveFromContinueReading": "Ta bort från Fortsätt läsa", + "ButtonRemoveSeriesFromContinueSeries": "Ta bort serie från Fortsätt serie", + "ButtonReScan": "Omstart", + "ButtonReset": "Återställ", + "ButtonResetToDefault": "Återställ till standard", + "ButtonRestore": "Återställ", + "ButtonSave": "Spara", + "ButtonSaveAndClose": "Spara och stäng", + "ButtonSaveTracklist": "Spara spårlista", + "ButtonScan": "Skanna", + "ButtonScanLibrary": "Skanna bibliotek", + "ButtonSearch": "Sök", + "ButtonSelectFolderPath": "Välj mappens sökväg", + "ButtonSeries": "Serie", + "ButtonSetChaptersFromTracks": "Ställ in kapitel från spår", + "ButtonShiftTimes": "Förskjut tider", + "ButtonShow": "Visa", + "ButtonStartM4BEncode": "Starta M4B-kodning", + "ButtonStartMetadataEmbed": "Starta inbäddning av metadata", + "ButtonSubmit": "Skicka", + "ButtonTest": "Testa", + "ButtonUpload": "Ladda upp", + "ButtonUploadBackup": "Ladda upp säkerhetskopia", + "ButtonUploadCover": "Ladda upp omslag", + "ButtonUploadOPMLFile": "Ladda upp OPML-fil", + "ButtonUserDelete": "Radera användare {0}", + "ButtonUserEdit": "Redigera användare {0}", + "ButtonViewAll": "Visa alla", + "ButtonYes": "Ja", + "HeaderAccount": "Konto", + "HeaderAdvanced": "Avancerad", + "HeaderAppriseNotificationSettings": "Apprise Meddelandeinställningar", + "HeaderAudiobookTools": "Ljudbokshantering", + "HeaderAudioTracks": "Ljudspår", + "HeaderBackups": "Säkerhetskopior", + "HeaderChangePassword": "Ändra lösenord", + "HeaderChapters": "Kapitel", + "HeaderChooseAFolder": "Välj en mapp", + "HeaderCollection": "Samling", + "HeaderCollectionItems": "Samlingselement", + "HeaderCover": "Omslag", + "HeaderCurrentDownloads": "Aktuella nedladdningar", + "HeaderDetails": "Detaljer", + "HeaderDownloadQueue": "Nedladdningskö", + "HeaderEbookFiles": "E-boksfiler", + "HeaderEmail": "E-post", + "HeaderEmailSettings": "E-postinställningar", + "HeaderEpisodes": "Avsnitt", + "HeaderEreaderDevices": "E-boksläsarenheter", + "HeaderEreaderSettings": "E-boksinställningar", + "HeaderFiles": "Filer", + "HeaderFindChapters": "Hitta kapitel", + "HeaderIgnoredFiles": "Ignorerade filer", + "HeaderItemFiles": "Föremålsfiler", + "HeaderItemMetadataUtils": "Metadataverktyg för föremål", + "HeaderLastListeningSession": "Senaste lyssningssession", + "HeaderLatestEpisodes": "Senaste avsnitt", + "HeaderLibraries": "Bibliotek", + "HeaderLibraryFiles": "Biblioteksfiler", + "HeaderLibraryStats": "Biblioteksstatistik", + "HeaderListeningSessions": "Lyssningssessioner", + "HeaderListeningStats": "Lyssningsstatistik", + "HeaderLogin": "Logga in", + "HeaderLogs": "Loggar", + "HeaderManageGenres": "Hantera genrer", + "HeaderManageTags": "Hantera taggar", + "HeaderMapDetails": "Karta detaljer", + "HeaderMatch": "Matcha", + "HeaderMetadataOrderOfPrecedence": "Metadataordning av företräde", + "HeaderMetadataToEmbed": "Metadata att bädda in", + "HeaderNewAccount": "Nytt konto", + "HeaderNewLibrary": "Nytt bibliotek", + "HeaderNotifications": "Meddelanden", + "HeaderOpenRSSFeed": "Öppna RSS-flöde", + "HeaderOtherFiles": "Andra filer", + "HeaderPermissions": "Behörigheter", + "HeaderPlayerQueue": "Spelarkö", + "HeaderPlaylist": "Spellista", + "HeaderPlaylistItems": "Spellistobjekt", + "HeaderPodcastsToAdd": "Podcaster att lägga till", + "HeaderPreviewCover": "Förhandsgranska omslag", + "HeaderRemoveEpisode": "Ta bort avsnitt", + "HeaderRemoveEpisodes": "Ta bort {0} avsnitt", + "HeaderRSSFeedGeneral": "RSS-information", + "HeaderRSSFeedIsOpen": "RSS-flödet är öppet", + "HeaderRSSFeeds": "RSS-flöden", + "HeaderSavedMediaProgress": "Sparad medieförlopp", + "HeaderSchedule": "Schema", + "HeaderScheduleLibraryScans": "Schemalagda biblioteksskanningar", + "HeaderSession": "Session", + "HeaderSetBackupSchedule": "Ange schemaläggning för säkerhetskopia", + "HeaderSettings": "Inställningar", + "HeaderSettingsDisplay": "Visning", + "HeaderSettingsExperimental": "Experimentella funktioner", + "HeaderSettingsGeneral": "Allmänt", + "HeaderSettingsScanner": "Skanner", + "HeaderSleepTimer": "Sovtidtagare", + "HeaderStatsLargestItems": "Största föremål", + "HeaderStatsLongestItems": "Längsta föremål (tim)", + "HeaderStatsMinutesListeningChart": "Minuters lyssning (senaste 7 dagar)", + "HeaderStatsRecentSessions": "Senaste sessioner", + "HeaderStatsTop10Authors": "Topp 10 författare", + "HeaderStatsTop5Genres": "Topp 5 genrer", + "HeaderTableOfContents": "Innehållsförteckning", + "HeaderTools": "Verktyg", + "HeaderUpdateAccount": "Uppdatera konto", + "HeaderUpdateAuthor": "Uppdatera författare", + "HeaderUpdateDetails": "Uppdatera detaljer", + "HeaderUpdateLibrary": "Uppdatera bibliotek", + "HeaderUsers": "Användare", + "HeaderYourStats": "Dina statistik", + "LabelAbridged": "Förkortad", + "LabelAccountType": "Kontotyp", + "LabelAccountTypeAdmin": "Admin", + "LabelAccountTypeGuest": "Gäst", + "LabelAccountTypeUser": "Användare", + "LabelActivity": "Aktivitet", + "LabelAdded": "Tillagd", + "LabelAddedAt": "Tillagd vid", + "LabelAddToCollection": "Lägg till i Samling", + "LabelAddToCollectionBatch": "Lägg till {0} böcker i Samlingen", + "LabelAddToPlaylist": "Lägg till i Spellista", + "LabelAddToPlaylistBatch": "Lägg till {0} objekt i Spellistan", + "LabelAdminUsersOnly": "Endast administratörer", + "LabelAll": "Alla", + "LabelAllUsers": "Alla användare", + "LabelAllUsersExcludingGuests": "Alla användare utom gäster", + "LabelAllUsersIncludingGuests": "Alla användare inklusive gäster", + "LabelAlreadyInYourLibrary": "Redan i din samling", + "LabelAppend": "Lägg till", + "LabelAuthor": "Författare", + "LabelAuthorFirstLast": "Författare (Förnamn Efternamn)", + "LabelAuthorLastFirst": "Författare (Efternamn, Förnamn)", + "LabelAuthors": "Författare", + "LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt", + "LabelBackToUser": "Tillbaka till användaren", + "LabelBackupLocation": "Säkerhetskopia Plats", + "LabelBackupsEnableAutomaticBackups": "Aktivera automatiska säkerhetskopior", + "LabelBackupsEnableAutomaticBackupsHelp": "Säkerhetskopior sparas i /metadata/säkerhetskopior", + "LabelBackupsMaxBackupSize": "Maximal säkerhetskopiostorlek (i GB)", + "LabelBackupsMaxBackupSizeHelp": "Som ett skydd mot felkonfiguration kommer säkerhetskopior att misslyckas om de överskrider den konfigurerade storleken.", + "LabelBackupsNumberToKeep": "Antal säkerhetskopior att behålla", + "LabelBackupsNumberToKeepHelp": "Endast en säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än detta bör du ta bort dem manuellt.", + "LabelBitrate": "Bitfrekvens", + "LabelBooks": "Böcker", + "LabelChangePassword": "Ändra lösenord", + "LabelChannels": "Kanaler", + "LabelChapters": "Kapitel", + "LabelChaptersFound": "hittade kapitel", + "LabelChapterTitle": "Kapitelrubrik", + "LabelClickForMoreInfo": "Klicka för mer information", + "LabelClosePlayer": "Stäng spelaren", + "LabelCodec": "Codec", + "LabelCollapseSeries": "Fäll ihop serie", + "LabelCollection": "Samling", + "LabelCollections": "Samlingar", + "LabelComplete": "Komplett", + "LabelConfirmPassword": "Bekräfta lösenord", + "LabelContinueListening": "Fortsätt lyssna", + "LabelContinueReading": "Fortsätt läsa", + "LabelContinueSeries": "Fortsätt serie", + "LabelCover": "Omslag", + "LabelCoverImageURL": "URL till omslagsbild", + "LabelCreatedAt": "Skapad vid", + "LabelCronExpression": "Cron-uttryck", + "LabelCurrent": "Nuvarande", + "LabelCurrently": "För närvarande:", + "LabelCustomCronExpression": "Anpassat Cron-uttryck:", + "LabelDatetime": "Datum och tid", + "LabelDeleteFromFileSystemCheckbox": "Ta bort från filsystem (avmarkera för att endast ta bort från databasen)", + "LabelDescription": "Beskrivning", + "LabelDeselectAll": "Avmarkera alla", + "LabelDevice": "Enhet", + "LabelDeviceInfo": "Enhetsinformation", + "LabelDeviceIsAvailableTo": "Enhet är tillgänglig för...", + "LabelDirectory": "Katalog", + "LabelDiscFromFilename": "Skiva från filnamn", + "LabelDiscFromMetadata": "Skiva från metadata", + "LabelDiscover": "Upptäck", + "LabelDownload": "Ladda ner", + "LabelDownloadNEpisodes": "Ladda ner {0} avsnitt", + "LabelDuration": "Varaktighet", + "LabelDurationFound": "Varaktighet hittad:", + "LabelEbook": "E-bok", + "LabelEbooks": "E-böcker", + "LabelEdit": "Redigera", + "LabelEmail": "E-post", + "LabelEmailSettingsFromAddress": "Från adress", + "LabelEmailSettingsSecure": "Säker", + "LabelEmailSettingsSecureHelp": "Om sant kommer anslutningen att använda TLS vid anslutning till servern. Om falskt används TLS om servern stöder STARTTLS-tillägget. I de flesta fall, om du ansluter till port 465, bör du ställa in detta värde till sant. För port 587 eller 25, låt det vara falskt. (från nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Testadress", + "LabelEmbeddedCover": "Inbäddat omslag", + "LabelEnable": "Aktivera", + "LabelEnd": "Slut", + "LabelEpisode": "Avsnitt", + "LabelEpisodeTitle": "Avsnittsrubrik", + "LabelEpisodeType": "Avsnittstyp", + "LabelExample": "Exempel", + "LabelExplicit": "Explicit", + "LabelFeedURL": "Flödes-URL", + "LabelFile": "Fil", + "LabelFileBirthtime": "Födelse-tidpunkt för fil", + "LabelFileModified": "Fil ändrad", + "LabelFilename": "Filnamn", + "LabelFilterByUser": "Filtrera efter användare", + "LabelFindEpisodes": "Hitta avsnitt", + "LabelFinished": "Avslutad", + "LabelFolder": "Mapp", + "LabelFolders": "Mappar", + "LabelFontFamily": "Teckensnittsfamilj", + "LabelFontScale": "Teckensnittsskala", + "LabelFormat": "Format", + "LabelGenre": "Genre", + "LabelGenres": "Genrer", + "LabelHardDeleteFile": "Hård radering av fil", + "LabelHasEbook": "Har e-bok", + "LabelHasSupplementaryEbook": "Har kompletterande e-bok", + "LabelHost": "Värd", + "LabelHour": "Timme", + "LabelIcon": "Ikon", + "LabelImageURLFromTheWeb": "Bild-URL från webben", + "LabelIncludeInTracklist": "Inkludera i spårlista", + "LabelIncomplete": "Ofullständig", + "LabelInProgress": "Pågående", + "LabelInterval": "Intervall", + "LabelIntervalCustomDailyWeekly": "Anpassat dagligt/veckovis", + "LabelIntervalEvery12Hours": "Var 12:e timme", + "LabelIntervalEvery15Minutes": "Var 15:e minut", + "LabelIntervalEvery2Hours": "Var 2:e timme", + "LabelIntervalEvery30Minutes": "Var 30:e minut", + "LabelIntervalEvery6Hours": "Var 6:e timme", + "LabelIntervalEveryDay": "Varje dag", + "LabelIntervalEveryHour": "Varje timme", + "LabelInvalidParts": "Ogiltiga delar", + "LabelInvert": "Invertera", + "LabelItem": "Objekt", + "LabelLanguage": "Språk", + "LabelLanguageDefaultServer": "Standardspråk för server", + "LabelLastBookAdded": "Senaste bok tillagd", + "LabelLastBookUpdated": "Senaste bok uppdaterad", + "LabelLastSeen": "Senast sedd", + "LabelLastTime": "Senaste gången", + "LabelLastUpdate": "Senaste uppdatering", + "LabelLayout": "Layout", + "LabelLayoutSinglePage": "En sida", + "LabelLayoutSplitPage": "Dela sida", + "LabelLess": "Mindre", + "LabelLibrariesAccessibleToUser": "Åtkomliga bibliotek för användare", + "LabelLibrary": "Bibliotek", + "LabelLibraryItem": "Biblioteksobjekt", + "LabelLibraryName": "Biblioteksnamn", + "LabelLimit": "Begränsning", + "LabelLineSpacing": "Radavstånd", + "LabelListenAgain": "Lyssna igen", + "LabelLogLevelDebug": "Felsökningsnivå: Felsökning", + "LabelLogLevelInfo": "Felsökningsnivå: Information", + "LabelLogLevelWarn": "Felsökningsnivå: Varning", + "LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum", + "LabelMediaPlayer": "Mediaspelare", + "LabelMediaType": "Mediatyp", + "LabelMetadataOrderOfPrecedenceDescription": "1 är lägsta prioritet, 5 är högsta prioritet", + "LabelMetadataProvider": "Metadataleverantör", + "LabelMetaTag": "Metamärke", + "LabelMetaTags": "Metamärken", + "LabelMinute": "Minut", + "LabelMissing": "Saknad", + "LabelMissingParts": "Saknade delar", + "LabelMore": "Mer", + "LabelMoreInfo": "Mer information", + "LabelName": "Namn", + "LabelNarrator": "Berättare", + "LabelNarrators": "Berättare", + "LabelNew": "Ny", + "LabelNewestAuthors": "Nyaste författare", + "LabelNewestEpisodes": "Nyaste avsnitt", + "LabelNewPassword": "Nytt lösenord", + "LabelNextBackupDate": "Nästa säkerhetskopia datum", + "LabelNextScheduledRun": "Nästa schemalagda körning", + "LabelNoEpisodesSelected": "Inga avsnitt valda", + "LabelNotes": "Anteckningar", + "LabelNotFinished": "Ej avslutad", + "LabelNotificationAppriseURL": "Apprise URL(er)", + "LabelNotificationAvailableVariables": "Tillgängliga variabler", + "LabelNotificationBodyTemplate": "Kroppsmall", + "LabelNotificationEvent": "Aviseringshändelse", + "LabelNotificationsMaxFailedAttempts": "Max antal misslyckade försök", + "LabelNotificationsMaxFailedAttemptsHelp": "Aviseringar inaktiveras när de misslyckas med att skickas så många gånger", + "LabelNotificationsMaxQueueSize": "Max köstorlek för aviseringsevenemang", + "LabelNotificationsMaxQueueSizeHelp": "Evenemang är begränsade till att utlösa ett per sekund. Evenemang kommer att ignoreras om kön är full. Detta förhindrar aviseringsspam.", + "LabelNotificationTitleTemplate": "Titelsmall", + "LabelNotStarted": "Inte påbörjad", + "LabelNumberOfBooks": "Antal böcker", + "LabelNumberOfEpisodes": "Antal avsnitt", + "LabelOpenRSSFeed": "Öppna RSS-flöde", + "LabelOverwrite": "Skriv över", + "LabelPassword": "Lösenord", + "LabelPath": "Sökväg", + "LabelPermissionsAccessAllLibraries": "Kan komma åt alla bibliotek", + "LabelPermissionsAccessAllTags": "Kan komma åt alla taggar", + "LabelPermissionsAccessExplicitContent": "Kan komma åt explicit innehåll", + "LabelPermissionsDelete": "Kan radera", + "LabelPermissionsDownload": "Kan ladda ner", + "LabelPermissionsUpdate": "Kan uppdatera", + "LabelPermissionsUpload": "Kan ladda upp", + "LabelPhotoPathURL": "Bildsökväg/URL", + "LabelPlaylists": "Spellistor", + "LabelPlayMethod": "Spelläge", + "LabelPodcast": "Podcast", + "LabelPodcasts": "Podcasts", + "LabelPodcastType": "Podcasttyp", + "LabelPort": "Port", + "LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)", + "LabelPreventIndexing": "Förhindra att ditt flöde indexeras av iTunes och Google-podcastsökmotorer", + "LabelPrimaryEbook": "Primär e-bok", + "LabelProgress": "Framsteg", + "LabelProvider": "Leverantör", + "LabelPubDate": "Publiceringsdatum", + "LabelPublisher": "Utgivare", + "LabelPublishYear": "Publiceringsår", + "LabelRead": "Läst", + "LabelReadAgain": "Läs igen", + "LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg", + "LabelRecentlyAdded": "Nyligen tillagd", + "LabelRecentSeries": "Senaste serier", + "LabelRecommended": "Rekommenderad", + "LabelRegion": "Region", + "LabelReleaseDate": "Utgivningsdatum", + "LabelRemoveCover": "Ta bort omslag", + "LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post", + "LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn", + "LabelRSSFeedOpen": "Öppna RSS-flöde", + "LabelRSSFeedPreventIndexing": "Förhindra indexering", + "LabelRSSFeedSlug": "RSS-flödesslag", + "LabelRSSFeedURL": "RSS-flöde URL", + "LabelSearchTerm": "Sökterm", + "LabelSearchTitle": "Sök titel", + "LabelSearchTitleOrASIN": "Sök titel eller ASIN", + "LabelSeason": "Säsong", + "LabelSelectAllEpisodes": "Välj alla avsnitt", + "LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas", + "LabelSelectUsers": "Välj användare", + "LabelSendEbookToDevice": "Skicka e-bok till...", + "LabelSequence": "Sekvens", + "LabelSeries": "Serie", + "LabelSeriesName": "Serienamn", + "LabelSeriesProgress": "Serieframsteg", + "LabelSetEbookAsPrimary": "Ange som primär", + "LabelSetEbookAsSupplementary": "Ange som kompletterande", + "LabelSettingsAudiobooksOnly": "Endast ljudböcker", + "LabelSettingsAudiobooksOnlyHelp": "Aktivera detta alternativ kommer att ignorera e-boksfiler om de inte finns inom en ljudboksmapp, i vilket fall de kommer att anges som kompletterande e-böcker", + "LabelSettingsBookshelfViewHelp": "Skeumorfisk design med trähyllor", + "LabelSettingsChromecastSupport": "Chromecast-stöd", + "LabelSettingsDateFormat": "Datumformat", + "LabelSettingsDisableWatcher": "Inaktivera Watcher", + "LabelSettingsDisableWatcherForLibrary": "Inaktivera mappbevakning för bibliotek", + "LabelSettingsDisableWatcherHelp": "Inaktiverar automatiskt lägga till/uppdatera objekt när filändringar upptäcks. *Kräver omstart av servern", + "LabelSettingsEnableWatcher": "Aktivera Watcher", + "LabelSettingsEnableWatcherForLibrary": "Aktivera mappbevakning för bibliotek", + "LabelSettingsEnableWatcherHelp": "Aktiverar automatiskt lägga till/uppdatera objekt när filändringar upptäcks. *Kräver omstart av servern", + "LabelSettingsExperimentalFeatures": "Experimentella funktioner", + "LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.", + "LabelSettingsFindCovers": "Hitta omslag", + "LabelSettingsFindCoversHelp": "Om din ljudbok inte har ett inbäddat omslag eller en omslagsbild i mappen kommer skannern att försöka hitta ett omslag.<br>Observera: Detta kommer att förlänga skannningstiden", + "LabelSettingsHideSingleBookSeries": "Dölj enboksserier", + "LabelSettingsHideSingleBookSeriesHelp": "Serier som har en enda bok kommer att döljas från seriesidan och hyllsidan på startsidan.", + "LabelSettingsHomePageBookshelfView": "Startsida använd bokhyllvy", + "LabelSettingsLibraryBookshelfView": "Bibliotek använd bokhyllvy", + "LabelSettingsParseSubtitles": "Analysera undertexter", + "LabelSettingsParseSubtitlesHelp": "Extrahera undertexter från mappnamn för ljudböcker.<br>Undertext måste vara åtskilda av \" - \"<br>t.ex. \"Boktitel - En undertitel här\" har undertiteln \"En undertitel här\"", + "LabelSettingsPreferMatchedMetadata": "Föredra matchad metadata", + "LabelSettingsPreferMatchedMetadataHelp": "Matchad data kommer att åsidosätta objektdetaljer vid snabbmatchning. Som standard kommer snabbmatchning endast att fylla i saknade detaljer.", + "LabelSettingsSkipMatchingBooksWithASIN": "Hoppa över matchande böcker med ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Hoppa över matchande böcker med ISBN", + "LabelSettingsSortingIgnorePrefixes": "Ignorera prefix vid sortering", + "LabelSettingsSortingIgnorePrefixesHelp": "t.ex. för prefixet \"the\" kommer boktiteln \"The Book Title\" att sorteras som \"Book Title, The\"", + "LabelSettingsSquareBookCovers": "Använd fyrkantiga bokomslag", + "LabelSettingsSquareBookCoversHelp": "Föredrar att använda fyrkantiga omslag över standard 1.6:1 bokomslag", + "LabelSettingsStoreCoversWithItem": "Lagra omslag med objekt", + "LabelSettingsStoreCoversWithItemHelp": "Som standard lagras omslag i /metadata/items, att aktivera detta alternativ kommer att lagra omslag i din biblioteksmapp. Endast en fil med namnet \"cover\" kommer att behållas", + "LabelSettingsStoreMetadataWithItem": "Lagra metadata med objekt", + "LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i /metadata/items, att aktivera detta alternativ kommer att lagra metadatafiler i dina biblioteksmappar", + "LabelSettingsTimeFormat": "Tidsformat", + "LabelShowAll": "Visa alla", + "LabelSize": "Storlek", + "LabelSleepTimer": "Sleeptimer", + "LabelSlug": "Slug", + "LabelStart": "Start", + "LabelStarted": "Startad", + "LabelStartedAt": "Startad vid", + "LabelStartTime": "Starttid", + "LabelStatsAudioTracks": "Ljudspår", + "LabelStatsAuthors": "Författare", + "LabelStatsBestDay": "Bästa dag", + "LabelStatsDailyAverage": "Dagligt genomsnitt", + "LabelStatsDays": "Dagar", + "LabelStatsDaysListened": "Dagar lyssnade", + "LabelStatsHours": "Timmar", + "LabelStatsInARow": "i rad", + "LabelStatsItemsFinished": "Objekt avslutade", + "LabelStatsItemsInLibrary": "Objekt i biblioteket", + "LabelStatsMinutes": "minuter", + "LabelStatsMinutesListening": "Minuter av lyssnande", + "LabelStatsOverallDays": "Totalt antal dagar", + "LabelStatsOverallHours": "Totalt antal timmar", + "LabelStatsWeekListening": "Veckans lyssnande", + "LabelSubtitle": "Underrubrik", + "LabelSupportedFileTypes": "Stödda filtyper", + "LabelTag": "Tagg", + "LabelTags": "Taggar", + "LabelTagsAccessibleToUser": "Taggar tillgängliga för användaren", + "LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren", + "LabelTasks": "Körande uppgifter", + "LabelTheme": "Tema", + "LabelThemeDark": "Mörkt", + "LabelThemeLight": "Ljust", + "LabelTimeBase": "Tidsbas", + "LabelTimeListened": "Tid lyssnad", + "LabelTimeListenedToday": "Tid lyssnad idag", + "LabelTimeRemaining": "{0} kvar", + "LabelTimeToShift": "Tid att skifta i sekunder", + "LabelTitle": "Titel", + "LabelToolsEmbedMetadata": "Bädda in metadata", + "LabelToolsEmbedMetadataDescription": "Bädda in metadata i ljudfiler, inklusive omslagsbild och kapitel.", + "LabelToolsMakeM4b": "Skapa M4B ljudbok", + "LabelToolsMakeM4bDescription": "Skapa en .M4B ljudboksfil med inbäddad metadata, omslagsbild och kapitel.", + "LabelToolsSplitM4b": "Dela M4B till MP3-filer", + "LabelToolsSplitM4bDescription": "Skapa MP3-filer från en M4B fil uppdelad i kapitel med inbäddad metadata, omslagsbild och kapitel.", + "LabelTotalDuration": "Total varaktighet", + "LabelTotalTimeListened": "Total tid lyssnad", + "LabelTrackFromFilename": "Spår från filnamn", + "LabelTrackFromMetadata": "Spår från metadata", + "LabelTracks": "Spår", + "LabelTracksMultiTrack": "Flerspårigt", + "LabelTracksNone": "Inga spår", + "LabelTracksSingleTrack": "Enspårigt", + "LabelType": "Typ", + "LabelUnabridged": "Oavkortad", + "LabelUnknown": "Okänd", + "LabelUpdateCover": "Uppdatera omslag", + "LabelUpdateCoverHelp": "Tillåt överskrivning av befintliga omslag för de valda böckerna när en matchning hittas", + "LabelUpdatedAt": "Uppdaterad vid", + "LabelUpdateDetails": "Uppdatera detaljer", + "LabelUpdateDetailsHelp": "Tillåt överskrivning av befintliga detaljer för de valda böckerna när en matchning hittas", + "LabelUploaderDragAndDrop": "Dra och släpp filer eller mappar", + "LabelUploaderDropFiles": "Släpp filer", + "LabelUseChapterTrack": "Använd kapitelspår", + "LabelUseFullTrack": "Använd hela spåret", + "LabelUser": "Användare", + "LabelUsername": "Användarnamn", + "LabelValue": "Värde", + "LabelVersion": "Version", + "LabelViewBookmarks": "Visa bokmärken", + "LabelViewChapters": "Visa kapitel", + "LabelViewQueue": "Visa spellista", + "LabelVolume": "Volym", + "LabelWeekdaysToRun": "Vardagar att köra", + "LabelYourAudiobookDuration": "Din ljudboks varaktighet", + "LabelYourBookmarks": "Dina bokmärken", + "LabelYourPlaylists": "Dina spellistor", + "LabelYourProgress": "Din framsteg", + "MessageAddToPlayerQueue": "Lägg till i spellistan", + "MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> igång eller en API som hanterar dessa begäranden. <br />Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på <code>http://192.168.1.1:8337</code>, bör du ange <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "Säkerhetskopieringar inkluderar användare, användares framsteg, biblioteksföremål, serverinställningar och bilder lagrade i <code>/metadata/items</code> & <code>/metadata/authors</code>. Säkerhetskopieringar inkluderar <strong>inte</strong> några filer lagrade i dina biblioteksmappar.", + "MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.", + "MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar", + "MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna", + "MessageBookshelfNoSeries": "Du har inga serier", + "MessageChapterEndIsAfter": "Kapitelns slut är efter din ljudboks slut", + "MessageChapterErrorFirstNotZero": "Första kapitlet måste börja vid 0", + "MessageChapterErrorStartGteDuration": "Ogiltig starttid måste vara mindre än ljudbokens varaktighet", + "MessageChapterErrorStartLtPrev": "Ogiltig starttid måste vara större än eller lika med tidigare kapitels starttid", + "MessageChapterStartIsAfter": "Kapitlets start är efter din ljudboks slut", + "MessageCheckingCron": "Kontrollerar cron...", + "MessageConfirmCloseFeed": "Är du säker på att du vill stänga detta flöde?", + "MessageConfirmDeleteBackup": "Är du säker på att du vill radera säkerhetskopian för {0}?", + "MessageConfirmDeleteFile": "Detta kommer att radera filen från ditt filsystem. Är du säker?", + "MessageConfirmDeleteLibrary": "Är du säker på att du vill radera biblioteket \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "Detta kommer att radera biblioteksföremålet från databasen och ditt filsystem. Är du säker?", + "MessageConfirmDeleteLibraryItems": "Detta kommer att radera {0} biblioteksföremål från databasen och ditt filsystem. Är du säker?", + "MessageConfirmDeleteSession": "Är du säker på att du vill radera denna session?", + "MessageConfirmForceReScan": "Är du säker på att du vill tvinga omgenomsökning?", + "MessageConfirmMarkAllEpisodesFinished": "Är du säker på att du vill markera alla avsnitt som avslutade?", + "MessageConfirmMarkAllEpisodesNotFinished": "Är du säker på att du vill markera alla avsnitt som inte avslutade?", + "MessageConfirmMarkSeriesFinished": "Är du säker på att du vill markera alla böcker i denna serie som avslutade?", + "MessageConfirmMarkSeriesNotFinished": "Är du säker på att du vill markera alla böcker i denna serie som inte avslutade?", + "MessageConfirmQuickEmbed": "Varning! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler. <br><br>Vill du fortsätta?", + "MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?", + "MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?", + "MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?", + "MessageConfirmRemoveEpisode": "Är du säker på att du vill ta bort avsnittet \"{0}\"?", + "MessageConfirmRemoveEpisodes": "Är du säker på att du vill ta bort {0} avsnitt?", + "MessageConfirmRemoveNarrator": "Är du säker på att du vill ta bort berättaren \"{0}\"?", + "MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?", + "MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på genren \"{0}\" till \"{1}\" för alla objekt?", + "MessageConfirmRenameGenreMergeNote": "Observera: Den här genren finns redan, så de kommer att slås samman.", + "MessageConfirmRenameGenreWarning": "Varning! En liknande genre med annat skrivsätt finns redan \"{0}\".", + "MessageConfirmRenameTag": "Är du säker på att du vill byta namn på taggen \"{0}\" till \"{1}\" för alla objekt?", + "MessageConfirmRenameTagMergeNote": "Observera: Den här taggen finns redan, så de kommer att slås samman.", + "MessageConfirmRenameTagWarning": "Varning! En liknande tagg med annat skrivsätt finns redan \"{0}\".", + "MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra omgenomsökning för {0} objekt?", + "MessageConfirmSendEbookToDevice": "Är du säker på att du vill skicka {0} e-bok \"{1}\" till enheten \"{2}\"?", + "MessageDownloadingEpisode": "Laddar ner avsnitt", + "MessageDragFilesIntoTrackOrder": "Dra filer till rätt spårordning", + "MessageEmbedFinished": "Inbäddning klar!", + "MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning", + "MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}", + "MessageFetching": "Hämtar...", + "MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.", + "MessageImportantNotice": "Viktig meddelande!", + "MessageInsertChapterBelow": "Infoga kapitel nedanför", + "MessageItemsSelected": "{0} Objekt markerade", + "MessageItemsUpdated": "{0} Objekt uppdaterade", + "MessageJoinUsOn": "Anslut dig till oss på", + "MessageListeningSessionsInTheLastYear": "{0} lyssningssessioner det senaste året", + "MessageLoading": "Laddar...", + "MessageLoadingFolders": "Laddar mappar...", + "MessageM4BFailed": "M4B misslyckades!", + "MessageM4BFinished": "M4B klar!", + "MessageMapChapterTitles": "Kartlägg kapitelrubriker till dina befintliga ljudbokskapitel utan att justera tidstämplar", + "MessageMarkAllEpisodesFinished": "Markera alla avsnitt som avslutade", + "MessageMarkAllEpisodesNotFinished": "Markera alla avsnitt som inte avslutade", + "MessageMarkAsFinished": "Markera som avslutad", + "MessageMarkAsNotFinished": "Markera som inte avslutad", + "MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från den valda sökleverantören och fylla i tomma detaljer och omslagskonst. Överskriver inte detaljer.", + "MessageNoAudioTracks": "Inga ljudspår", + "MessageNoAuthors": "Inga författare", + "MessageNoBackups": "Inga säkerhetskopior", + "MessageNoBookmarks": "Inga bokmärken", + "MessageNoChapters": "Inga kapitel", + "MessageNoCollections": "Inga samlingar", + "MessageNoCoversFound": "Inga omslag hittade", + "MessageNoDescription": "Ingen beskrivning", + "MessageNoDownloadsInProgress": "Inga nedladdningar pågår för närvarande", + "MessageNoDownloadsQueued": "Inga nedladdningar i kö", + "MessageNoEpisodeMatchesFound": "Inga matchande avsnitt hittades", + "MessageNoEpisodes": "Inga avsnitt", + "MessageNoFoldersAvailable": "Inga mappar tillgängliga", + "MessageNoGenres": "Inga genrer", + "MessageNoIssues": "Inga problem", + "MessageNoItems": "Inga objekt", + "MessageNoItemsFound": "Inga objekt hittades", + "MessageNoListeningSessions": "Inga lyssningssessioner", + "MessageNoLogs": "Inga loggar", + "MessageNoMediaProgress": "Ingen medieförlopp", + "MessageNoNotifications": "Inga aviseringar", + "MessageNoPodcastsFound": "Inga podcasts hittade", + "MessageNoResults": "Inga resultat", + "MessageNoSearchResultsFor": "Inga sökresultat för \"{0}\"", + "MessageNoSeries": "Inga serier", + "MessageNoTags": "Inga taggar", + "MessageNoTasksRunning": "Inga pågående uppgifter", + "MessageNotYetImplemented": "Ännu inte implementerad", + "MessageNoUpdateNecessary": "Ingen uppdatering krävs", + "MessageNoUpdatesWereNecessary": "Inga uppdateringar var nödvändiga", + "MessageNoUserPlaylists": "Du har inga spellistor", + "MessageOr": "eller", + "MessagePauseChapter": "Pausa kapiteluppspelning", + "MessagePlayChapter": "Lyssna på kapitlets början", + "MessagePlaylistCreateFromCollection": "Skapa spellista från samling", + "MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning", + "MessageQuickMatchDescription": "Fyll tomma objektdetaljer och omslag med första matchningsresultat från '{0}'. Överskriver inte detaljer om inte serverinställningen 'Föredra matchad metadata' är aktiverad.", + "MessageRemoveChapter": "Ta bort kapitel", + "MessageRemoveEpisodes": "Ta bort {0} avsnitt", + "MessageRemoveFromPlayerQueue": "Ta bort från spellistan", + "MessageRemoveUserWarning": "Är du säker på att du vill radera användaren \"{0}\" permanent?", + "MessageReportBugsAndContribute": "Rapportera buggar, begär funktioner och bidra på", + "MessageResetChaptersConfirm": "Är du säker på att du vill återställa kapitel och ångra ändringarna du gjort?", + "MessageRestoreBackupConfirm": "Är du säker på att du vill återställa säkerhetskopian som skapades den", + "MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.<br /><br />Säkerhetskopior ändrar inte några filer i dina biblioteksmappar. Om du har aktiverat serverinställningar för att lagra omslagskonst och metadata i dina biblioteksmappar säkerhetskopieras eller skrivs de inte över.<br /><br />Alla klienter som använder din server kommer att uppdateras automatiskt.", + "MessageSearchResultsFor": "Sökresultat för", + "MessageServerCouldNotBeReached": "Servern kunde inte nås", + "MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn", + "MessageStartPlaybackAtTime": "Starta uppspelning för \"{0}\" kl. {1}?", + "MessageThinking": "Tänker...", + "MessageUploaderItemFailed": "Misslyckades med att ladda upp", + "MessageUploaderItemSuccess": "Uppladdning lyckades!", + "MessageUploading": "Laddar upp...", + "MessageValidCronExpression": "Giltigt cron-uttryck", + "MessageWatcherIsDisabledGlobally": "Vakten är inaktiverad globalt i serverinställningarna", + "MessageXLibraryIsEmpty": "{0} biblioteket är tomt!", + "MessageYourAudiobookDurationIsLonger": "Varaktigheten på din ljudbok är längre än den hittade varaktigheten", + "MessageYourAudiobookDurationIsShorter": "Varaktigheten på din ljudbok är kortare än den hittade varaktigheten", + "NoteChangeRootPassword": "Rotanvändaren är den enda användaren som kan ha ett tomt lösenord", + "NoteChapterEditorTimes": "Obs: Starttiden för första kapitlet måste förbli 0:00 och starttiden för det sista kapitlet får inte överstiga ljudbokens varaktighet.", + "NoteFolderPicker": "Obs: Mappar som redan är kartlagda kommer inte att visas", + "NoteFolderPickerDebian": "Obs: Mappväljaren för Debian-installationen är inte fullständigt implementerad. Du bör ange sökvägen till ditt bibliotek direkt.", + "NoteRSSFeedPodcastAppsHttps": "Varning: De flesta podcastappar kräver att RSS-flödets URL används med HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "Varning: 1 eller flera av dina avsnitt har inte ett publiceringsdatum. Vissa podcastappar kräver detta.", + "NoteUploaderFoldersWithMediaFiles": "Mappar med mediefiler hanteras som separata biblioteksobjekt.", + "NoteUploaderOnlyAudioFiles": "Om du bara laddar upp ljudfiler kommer varje ljudfil att hanteras som en separat ljudbok.", + "NoteUploaderUnsupportedFiles": "Oaccepterade filer ignoreras. När du väljer eller släpper en mapp ignoreras andra filer som inte finns i ett objektmapp.", + "PlaceholderNewCollection": "Nytt samlingsnamn", + "PlaceholderNewFolderPath": "Nytt mappväg", + "PlaceholderNewPlaylist": "Nytt spellistanamn", + "PlaceholderSearch": "Sök...", + "PlaceholderSearchEpisode": "Sök avsnitt...", + "ToastAccountUpdateFailed": "Det gick inte att uppdatera kontot", + "ToastAccountUpdateSuccess": "Kontot uppdaterat", + "ToastAuthorImageRemoveFailed": "Det gick inte att ta bort författarens bild", + "ToastAuthorImageRemoveSuccess": "Författarens bild borttagen", + "ToastAuthorUpdateFailed": "Det gick inte att uppdatera författaren", + "ToastAuthorUpdateMerged": "Författaren sammanslagen", + "ToastAuthorUpdateSuccess": "Författaren uppdaterad", + "ToastAuthorUpdateSuccessNoImageFound": "Författaren uppdaterad (ingen bild hittad)", + "ToastBackupCreateFailed": "Det gick inte att skapa en säkerhetskopia", + "ToastBackupCreateSuccess": "Säkerhetskopia skapad", + "ToastBackupDeleteFailed": "Det gick inte att ta bort säkerhetskopian", + "ToastBackupDeleteSuccess": "Säkerhetskopan borttagen", + "ToastBackupRestoreFailed": "Det gick inte att återställa säkerhetskopan", + "ToastBackupUploadFailed": "Det gick inte att ladda upp säkerhetskopan", + "ToastBackupUploadSuccess": "Säkerhetskopan uppladdad", + "ToastBatchUpdateFailed": "Batchuppdateringen misslyckades", + "ToastBatchUpdateSuccess": "Batchuppdateringen lyckades", + "ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket", + "ToastBookmarkCreateSuccess": "Bokmärket tillagt", + "ToastBookmarkRemoveFailed": "Det gick inte att ta bort bokmärket", + "ToastBookmarkRemoveSuccess": "Bokmärket borttaget", + "ToastBookmarkUpdateFailed": "Det gick inte att uppdatera bokmärket", + "ToastBookmarkUpdateSuccess": "Bokmärket uppdaterat", + "ToastChaptersHaveErrors": "Kapitlen har fel", + "ToastChaptersMustHaveTitles": "Kapitel måste ha titlar", + "ToastCollectionItemsRemoveFailed": "Det gick inte att ta bort objekt från samlingen", + "ToastCollectionItemsRemoveSuccess": "Objekt borttagna från samlingen", + "ToastCollectionRemoveFailed": "Det gick inte att ta bort samlingen", + "ToastCollectionRemoveSuccess": "Samlingen borttagen", + "ToastCollectionUpdateFailed": "Det gick inte att uppdatera samlingen", + "ToastCollectionUpdateSuccess": "Samlingen uppdaterad", + "ToastItemCoverUpdateFailed": "Det gick inte att uppdatera objektets omslag", + "ToastItemCoverUpdateSuccess": "Objektets omslag uppdaterat", + "ToastItemDetailsUpdateFailed": "Det gick inte att uppdatera objektdetaljerna", + "ToastItemDetailsUpdateSuccess": "Objektdetaljer uppdaterade", + "ToastItemDetailsUpdateUnneeded": "Inga uppdateringar behövs för objektdetaljerna", + "ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera som färdig", + "ToastItemMarkedAsFinishedSuccess": "Objekt markerat som färdig", + "ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera som ej färdig", + "ToastItemMarkedAsNotFinishedSuccess": "Objekt markerat som ej färdig", + "ToastLibraryCreateFailed": "Det gick inte att skapa biblioteket", + "ToastLibraryCreateSuccess": "Biblioteket \"{0}\" skapat", + "ToastLibraryDeleteFailed": "Det gick inte att ta bort biblioteket", + "ToastLibraryDeleteSuccess": "Biblioteket borttaget", + "ToastLibraryScanFailedToStart": "Misslyckades med att starta skanningen", + "ToastLibraryScanStarted": "Skanning av biblioteket påbörjad", + "ToastLibraryUpdateFailed": "Det gick inte att uppdatera biblioteket", + "ToastLibraryUpdateSuccess": "Biblioteket \"{0}\" uppdaterat", + "ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan", + "ToastPlaylistCreateSuccess": "Spellistan skapad", + "ToastPlaylistRemoveFailed": "Det gick inte att ta bort spellistan", + "ToastPlaylistRemoveSuccess": "Spellistan borttagen", + "ToastPlaylistUpdateFailed": "Det gick inte att uppdatera spellistan", + "ToastPlaylistUpdateSuccess": "Spellistan uppdaterad", + "ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten", + "ToastPodcastCreateSuccess": "Podcasten skapad framgångsrikt", + "ToastRemoveItemFromCollectionFailed": "Misslyckades med att ta bort objektet från samlingen", + "ToastRemoveItemFromCollectionSuccess": "Objektet borttaget från samlingen", + "ToastRSSFeedCloseFailed": "Misslyckades med att stänga RSS-flödet", + "ToastRSSFeedCloseSuccess": "RSS-flödet stängt", + "ToastSendEbookToDeviceFailed": "Misslyckades med att skicka e-boken till enheten", + "ToastSendEbookToDeviceSuccess": "E-boken skickad till enheten \"{0}\"", + "ToastSeriesUpdateFailed": "Serieuppdateringen misslyckades", + "ToastSeriesUpdateSuccess": "Serieuppdateringen lyckades", + "ToastSessionDeleteFailed": "Misslyckades med att ta bort sessionen", + "ToastSessionDeleteSuccess": "Sessionen borttagen", + "ToastSocketConnected": "Socket ansluten", + "ToastSocketDisconnected": "Socket frånkopplad", + "ToastSocketFailedToConnect": "Socket misslyckades med att ansluta", + "ToastUserDeleteFailed": "Misslyckades med att ta bort användaren", + "ToastUserDeleteSuccess": "Användaren borttagen" + } From 89055f86552965e899a154c3517d22f85a966ee8 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sun, 5 Nov 2023 16:14:26 +0000 Subject: [PATCH 120/208] Remove unnecessary includesAuthorDiff from sorting --- server/finders/BookFinder.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index e3a84be09..fa034bce0 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -200,9 +200,6 @@ class BookFinder { getCandidates() { var candidates = [...this.candidates] candidates.sort((a, b) => { - // Candidates that include the author are likely low quality - const includesAuthorDiff = a.includes(this.cleanAuthor) - b.includes(this.cleanAuthor) - if (includesAuthorDiff) return includesAuthorDiff // Candidates that include only digits are also likely low quality const onlyDigits = /^\d+$/ const includesOnlyDigitsDiff = onlyDigits.test(a) - onlyDigits.test(b) From 910be21e936274c75bf97beb1752707d9edcc520 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 5 Nov 2023 10:16:40 -0600 Subject: [PATCH 121/208] Add Swedish language option --- client/plugins/i18n.js | 1 + client/strings/{se.json => sv.json} | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename client/strings/{se.json => sv.json} (99%) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 9a7eb02e8..ea6a06db7 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -18,6 +18,7 @@ const languageCodeMap = { 'no': { label: 'Norsk', dateFnsLocale: 'no' }, 'pl': { label: 'Polski', dateFnsLocale: 'pl' }, 'ru': { label: 'Русский', dateFnsLocale: 'ru' }, + 'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, } Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { diff --git a/client/strings/se.json b/client/strings/sv.json similarity index 99% rename from client/strings/se.json rename to client/strings/sv.json index f0580847c..23d489d05 100644 --- a/client/strings/se.json +++ b/client/strings/sv.json @@ -726,4 +726,4 @@ "ToastSocketFailedToConnect": "Socket misslyckades med att ansluta", "ToastUserDeleteFailed": "Misslyckades med att ta bort användaren", "ToastUserDeleteSuccess": "Användaren borttagen" - } +} \ No newline at end of file From 1e5d6a5d523c76d97c510c322186d0ae1ba849f3 Mon Sep 17 00:00:00 2001 From: Gustav Almstrom <gustav@almstrom.org> Date: Sun, 5 Nov 2023 16:51:45 +0100 Subject: [PATCH 122/208] Added swedish translation of strings --- client/strings/se.json | 729 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 729 insertions(+) create mode 100644 client/strings/se.json diff --git a/client/strings/se.json b/client/strings/se.json new file mode 100644 index 000000000..f0580847c --- /dev/null +++ b/client/strings/se.json @@ -0,0 +1,729 @@ +{ + "ButtonAdd": "Lägg till", + "ButtonAddChapters": "Lägg till kapitel", + "ButtonAddDevice": "Lägg till enhet", + "ButtonAddLibrary": "Lägg till bibliotek", + "ButtonAddPodcasts": "Lägg till podcasts", + "ButtonAddUser": "Lägg till användare", + "ButtonAddYourFirstLibrary": "Lägg till ditt första bibliotek", + "ButtonApply": "Tillämpa", + "ButtonApplyChapters": "Tillämpa kapitel", + "ButtonAuthors": "Författare", + "ButtonBrowseForFolder": "Bläddra efter mapp", + "ButtonCancel": "Avbryt", + "ButtonCancelEncode": "Avbryt kodning", + "ButtonChangeRootPassword": "Ändra rootlösenord", + "ButtonCheckAndDownloadNewEpisodes": "Kontrollera och ladda ner nya avsnitt", + "ButtonChooseAFolder": "Välj en mapp", + "ButtonChooseFiles": "Välj filer", + "ButtonClearFilter": "Rensa filter", + "ButtonCloseFeed": "Stäng flöde", + "ButtonCollections": "Samlingar", + "ButtonConfigureScanner": "Konfigurera skanner", + "ButtonCreate": "Skapa", + "ButtonCreateBackup": "Skapa säkerhetskopia", + "ButtonDelete": "Radera", + "ButtonDownloadQueue": "Kö", + "ButtonEdit": "Redigera", + "ButtonEditChapters": "Redigera kapitel", + "ButtonEditPodcast": "Redigera podcast", + "ButtonForceReScan": "Tvinga omstart", + "ButtonFullPath": "Full sökväg", + "ButtonHide": "Dölj", + "ButtonHome": "Hem", + "ButtonIssues": "Problem", + "ButtonLatest": "Senaste", + "ButtonLibrary": "Bibliotek", + "ButtonLogout": "Logga ut", + "ButtonLookup": "Sök", + "ButtonManageTracks": "Hantera spår", + "ButtonMapChapterTitles": "Karta kapitelrubriker", + "ButtonMatchAllAuthors": "Matcha alla författare", + "ButtonMatchBooks": "Matcha böcker", + "ButtonNevermind": "Glöm det", + "ButtonOk": "Okej", + "ButtonOpenFeed": "Öppna flöde", + "ButtonOpenManager": "Öppna Manager", + "ButtonPlay": "Spela", + "ButtonPlaying": "Spelar", + "ButtonPlaylists": "Spellistor", + "ButtonPurgeAllCache": "Rensa all cache", + "ButtonPurgeItemsCache": "Rensa föremåls-cache", + "ButtonPurgeMediaProgress": "Rensa medieförlopp", + "ButtonQueueAddItem": "Lägg till i kön", + "ButtonQueueRemoveItem": "Ta bort från kön", + "ButtonQuickMatch": "Snabb matchning", + "ButtonRead": "Läs", + "ButtonRemove": "Ta bort", + "ButtonRemoveAll": "Ta bort alla", + "ButtonRemoveAllLibraryItems": "Ta bort alla biblioteksobjekt", + "ButtonRemoveFromContinueListening": "Ta bort från Fortsätt lyssna", + "ButtonRemoveFromContinueReading": "Ta bort från Fortsätt läsa", + "ButtonRemoveSeriesFromContinueSeries": "Ta bort serie från Fortsätt serie", + "ButtonReScan": "Omstart", + "ButtonReset": "Återställ", + "ButtonResetToDefault": "Återställ till standard", + "ButtonRestore": "Återställ", + "ButtonSave": "Spara", + "ButtonSaveAndClose": "Spara och stäng", + "ButtonSaveTracklist": "Spara spårlista", + "ButtonScan": "Skanna", + "ButtonScanLibrary": "Skanna bibliotek", + "ButtonSearch": "Sök", + "ButtonSelectFolderPath": "Välj mappens sökväg", + "ButtonSeries": "Serie", + "ButtonSetChaptersFromTracks": "Ställ in kapitel från spår", + "ButtonShiftTimes": "Förskjut tider", + "ButtonShow": "Visa", + "ButtonStartM4BEncode": "Starta M4B-kodning", + "ButtonStartMetadataEmbed": "Starta inbäddning av metadata", + "ButtonSubmit": "Skicka", + "ButtonTest": "Testa", + "ButtonUpload": "Ladda upp", + "ButtonUploadBackup": "Ladda upp säkerhetskopia", + "ButtonUploadCover": "Ladda upp omslag", + "ButtonUploadOPMLFile": "Ladda upp OPML-fil", + "ButtonUserDelete": "Radera användare {0}", + "ButtonUserEdit": "Redigera användare {0}", + "ButtonViewAll": "Visa alla", + "ButtonYes": "Ja", + "HeaderAccount": "Konto", + "HeaderAdvanced": "Avancerad", + "HeaderAppriseNotificationSettings": "Apprise Meddelandeinställningar", + "HeaderAudiobookTools": "Ljudbokshantering", + "HeaderAudioTracks": "Ljudspår", + "HeaderBackups": "Säkerhetskopior", + "HeaderChangePassword": "Ändra lösenord", + "HeaderChapters": "Kapitel", + "HeaderChooseAFolder": "Välj en mapp", + "HeaderCollection": "Samling", + "HeaderCollectionItems": "Samlingselement", + "HeaderCover": "Omslag", + "HeaderCurrentDownloads": "Aktuella nedladdningar", + "HeaderDetails": "Detaljer", + "HeaderDownloadQueue": "Nedladdningskö", + "HeaderEbookFiles": "E-boksfiler", + "HeaderEmail": "E-post", + "HeaderEmailSettings": "E-postinställningar", + "HeaderEpisodes": "Avsnitt", + "HeaderEreaderDevices": "E-boksläsarenheter", + "HeaderEreaderSettings": "E-boksinställningar", + "HeaderFiles": "Filer", + "HeaderFindChapters": "Hitta kapitel", + "HeaderIgnoredFiles": "Ignorerade filer", + "HeaderItemFiles": "Föremålsfiler", + "HeaderItemMetadataUtils": "Metadataverktyg för föremål", + "HeaderLastListeningSession": "Senaste lyssningssession", + "HeaderLatestEpisodes": "Senaste avsnitt", + "HeaderLibraries": "Bibliotek", + "HeaderLibraryFiles": "Biblioteksfiler", + "HeaderLibraryStats": "Biblioteksstatistik", + "HeaderListeningSessions": "Lyssningssessioner", + "HeaderListeningStats": "Lyssningsstatistik", + "HeaderLogin": "Logga in", + "HeaderLogs": "Loggar", + "HeaderManageGenres": "Hantera genrer", + "HeaderManageTags": "Hantera taggar", + "HeaderMapDetails": "Karta detaljer", + "HeaderMatch": "Matcha", + "HeaderMetadataOrderOfPrecedence": "Metadataordning av företräde", + "HeaderMetadataToEmbed": "Metadata att bädda in", + "HeaderNewAccount": "Nytt konto", + "HeaderNewLibrary": "Nytt bibliotek", + "HeaderNotifications": "Meddelanden", + "HeaderOpenRSSFeed": "Öppna RSS-flöde", + "HeaderOtherFiles": "Andra filer", + "HeaderPermissions": "Behörigheter", + "HeaderPlayerQueue": "Spelarkö", + "HeaderPlaylist": "Spellista", + "HeaderPlaylistItems": "Spellistobjekt", + "HeaderPodcastsToAdd": "Podcaster att lägga till", + "HeaderPreviewCover": "Förhandsgranska omslag", + "HeaderRemoveEpisode": "Ta bort avsnitt", + "HeaderRemoveEpisodes": "Ta bort {0} avsnitt", + "HeaderRSSFeedGeneral": "RSS-information", + "HeaderRSSFeedIsOpen": "RSS-flödet är öppet", + "HeaderRSSFeeds": "RSS-flöden", + "HeaderSavedMediaProgress": "Sparad medieförlopp", + "HeaderSchedule": "Schema", + "HeaderScheduleLibraryScans": "Schemalagda biblioteksskanningar", + "HeaderSession": "Session", + "HeaderSetBackupSchedule": "Ange schemaläggning för säkerhetskopia", + "HeaderSettings": "Inställningar", + "HeaderSettingsDisplay": "Visning", + "HeaderSettingsExperimental": "Experimentella funktioner", + "HeaderSettingsGeneral": "Allmänt", + "HeaderSettingsScanner": "Skanner", + "HeaderSleepTimer": "Sovtidtagare", + "HeaderStatsLargestItems": "Största föremål", + "HeaderStatsLongestItems": "Längsta föremål (tim)", + "HeaderStatsMinutesListeningChart": "Minuters lyssning (senaste 7 dagar)", + "HeaderStatsRecentSessions": "Senaste sessioner", + "HeaderStatsTop10Authors": "Topp 10 författare", + "HeaderStatsTop5Genres": "Topp 5 genrer", + "HeaderTableOfContents": "Innehållsförteckning", + "HeaderTools": "Verktyg", + "HeaderUpdateAccount": "Uppdatera konto", + "HeaderUpdateAuthor": "Uppdatera författare", + "HeaderUpdateDetails": "Uppdatera detaljer", + "HeaderUpdateLibrary": "Uppdatera bibliotek", + "HeaderUsers": "Användare", + "HeaderYourStats": "Dina statistik", + "LabelAbridged": "Förkortad", + "LabelAccountType": "Kontotyp", + "LabelAccountTypeAdmin": "Admin", + "LabelAccountTypeGuest": "Gäst", + "LabelAccountTypeUser": "Användare", + "LabelActivity": "Aktivitet", + "LabelAdded": "Tillagd", + "LabelAddedAt": "Tillagd vid", + "LabelAddToCollection": "Lägg till i Samling", + "LabelAddToCollectionBatch": "Lägg till {0} böcker i Samlingen", + "LabelAddToPlaylist": "Lägg till i Spellista", + "LabelAddToPlaylistBatch": "Lägg till {0} objekt i Spellistan", + "LabelAdminUsersOnly": "Endast administratörer", + "LabelAll": "Alla", + "LabelAllUsers": "Alla användare", + "LabelAllUsersExcludingGuests": "Alla användare utom gäster", + "LabelAllUsersIncludingGuests": "Alla användare inklusive gäster", + "LabelAlreadyInYourLibrary": "Redan i din samling", + "LabelAppend": "Lägg till", + "LabelAuthor": "Författare", + "LabelAuthorFirstLast": "Författare (Förnamn Efternamn)", + "LabelAuthorLastFirst": "Författare (Efternamn, Förnamn)", + "LabelAuthors": "Författare", + "LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt", + "LabelBackToUser": "Tillbaka till användaren", + "LabelBackupLocation": "Säkerhetskopia Plats", + "LabelBackupsEnableAutomaticBackups": "Aktivera automatiska säkerhetskopior", + "LabelBackupsEnableAutomaticBackupsHelp": "Säkerhetskopior sparas i /metadata/säkerhetskopior", + "LabelBackupsMaxBackupSize": "Maximal säkerhetskopiostorlek (i GB)", + "LabelBackupsMaxBackupSizeHelp": "Som ett skydd mot felkonfiguration kommer säkerhetskopior att misslyckas om de överskrider den konfigurerade storleken.", + "LabelBackupsNumberToKeep": "Antal säkerhetskopior att behålla", + "LabelBackupsNumberToKeepHelp": "Endast en säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än detta bör du ta bort dem manuellt.", + "LabelBitrate": "Bitfrekvens", + "LabelBooks": "Böcker", + "LabelChangePassword": "Ändra lösenord", + "LabelChannels": "Kanaler", + "LabelChapters": "Kapitel", + "LabelChaptersFound": "hittade kapitel", + "LabelChapterTitle": "Kapitelrubrik", + "LabelClickForMoreInfo": "Klicka för mer information", + "LabelClosePlayer": "Stäng spelaren", + "LabelCodec": "Codec", + "LabelCollapseSeries": "Fäll ihop serie", + "LabelCollection": "Samling", + "LabelCollections": "Samlingar", + "LabelComplete": "Komplett", + "LabelConfirmPassword": "Bekräfta lösenord", + "LabelContinueListening": "Fortsätt lyssna", + "LabelContinueReading": "Fortsätt läsa", + "LabelContinueSeries": "Fortsätt serie", + "LabelCover": "Omslag", + "LabelCoverImageURL": "URL till omslagsbild", + "LabelCreatedAt": "Skapad vid", + "LabelCronExpression": "Cron-uttryck", + "LabelCurrent": "Nuvarande", + "LabelCurrently": "För närvarande:", + "LabelCustomCronExpression": "Anpassat Cron-uttryck:", + "LabelDatetime": "Datum och tid", + "LabelDeleteFromFileSystemCheckbox": "Ta bort från filsystem (avmarkera för att endast ta bort från databasen)", + "LabelDescription": "Beskrivning", + "LabelDeselectAll": "Avmarkera alla", + "LabelDevice": "Enhet", + "LabelDeviceInfo": "Enhetsinformation", + "LabelDeviceIsAvailableTo": "Enhet är tillgänglig för...", + "LabelDirectory": "Katalog", + "LabelDiscFromFilename": "Skiva från filnamn", + "LabelDiscFromMetadata": "Skiva från metadata", + "LabelDiscover": "Upptäck", + "LabelDownload": "Ladda ner", + "LabelDownloadNEpisodes": "Ladda ner {0} avsnitt", + "LabelDuration": "Varaktighet", + "LabelDurationFound": "Varaktighet hittad:", + "LabelEbook": "E-bok", + "LabelEbooks": "E-böcker", + "LabelEdit": "Redigera", + "LabelEmail": "E-post", + "LabelEmailSettingsFromAddress": "Från adress", + "LabelEmailSettingsSecure": "Säker", + "LabelEmailSettingsSecureHelp": "Om sant kommer anslutningen att använda TLS vid anslutning till servern. Om falskt används TLS om servern stöder STARTTLS-tillägget. I de flesta fall, om du ansluter till port 465, bör du ställa in detta värde till sant. För port 587 eller 25, låt det vara falskt. (från nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Testadress", + "LabelEmbeddedCover": "Inbäddat omslag", + "LabelEnable": "Aktivera", + "LabelEnd": "Slut", + "LabelEpisode": "Avsnitt", + "LabelEpisodeTitle": "Avsnittsrubrik", + "LabelEpisodeType": "Avsnittstyp", + "LabelExample": "Exempel", + "LabelExplicit": "Explicit", + "LabelFeedURL": "Flödes-URL", + "LabelFile": "Fil", + "LabelFileBirthtime": "Födelse-tidpunkt för fil", + "LabelFileModified": "Fil ändrad", + "LabelFilename": "Filnamn", + "LabelFilterByUser": "Filtrera efter användare", + "LabelFindEpisodes": "Hitta avsnitt", + "LabelFinished": "Avslutad", + "LabelFolder": "Mapp", + "LabelFolders": "Mappar", + "LabelFontFamily": "Teckensnittsfamilj", + "LabelFontScale": "Teckensnittsskala", + "LabelFormat": "Format", + "LabelGenre": "Genre", + "LabelGenres": "Genrer", + "LabelHardDeleteFile": "Hård radering av fil", + "LabelHasEbook": "Har e-bok", + "LabelHasSupplementaryEbook": "Har kompletterande e-bok", + "LabelHost": "Värd", + "LabelHour": "Timme", + "LabelIcon": "Ikon", + "LabelImageURLFromTheWeb": "Bild-URL från webben", + "LabelIncludeInTracklist": "Inkludera i spårlista", + "LabelIncomplete": "Ofullständig", + "LabelInProgress": "Pågående", + "LabelInterval": "Intervall", + "LabelIntervalCustomDailyWeekly": "Anpassat dagligt/veckovis", + "LabelIntervalEvery12Hours": "Var 12:e timme", + "LabelIntervalEvery15Minutes": "Var 15:e minut", + "LabelIntervalEvery2Hours": "Var 2:e timme", + "LabelIntervalEvery30Minutes": "Var 30:e minut", + "LabelIntervalEvery6Hours": "Var 6:e timme", + "LabelIntervalEveryDay": "Varje dag", + "LabelIntervalEveryHour": "Varje timme", + "LabelInvalidParts": "Ogiltiga delar", + "LabelInvert": "Invertera", + "LabelItem": "Objekt", + "LabelLanguage": "Språk", + "LabelLanguageDefaultServer": "Standardspråk för server", + "LabelLastBookAdded": "Senaste bok tillagd", + "LabelLastBookUpdated": "Senaste bok uppdaterad", + "LabelLastSeen": "Senast sedd", + "LabelLastTime": "Senaste gången", + "LabelLastUpdate": "Senaste uppdatering", + "LabelLayout": "Layout", + "LabelLayoutSinglePage": "En sida", + "LabelLayoutSplitPage": "Dela sida", + "LabelLess": "Mindre", + "LabelLibrariesAccessibleToUser": "Åtkomliga bibliotek för användare", + "LabelLibrary": "Bibliotek", + "LabelLibraryItem": "Biblioteksobjekt", + "LabelLibraryName": "Biblioteksnamn", + "LabelLimit": "Begränsning", + "LabelLineSpacing": "Radavstånd", + "LabelListenAgain": "Lyssna igen", + "LabelLogLevelDebug": "Felsökningsnivå: Felsökning", + "LabelLogLevelInfo": "Felsökningsnivå: Information", + "LabelLogLevelWarn": "Felsökningsnivå: Varning", + "LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum", + "LabelMediaPlayer": "Mediaspelare", + "LabelMediaType": "Mediatyp", + "LabelMetadataOrderOfPrecedenceDescription": "1 är lägsta prioritet, 5 är högsta prioritet", + "LabelMetadataProvider": "Metadataleverantör", + "LabelMetaTag": "Metamärke", + "LabelMetaTags": "Metamärken", + "LabelMinute": "Minut", + "LabelMissing": "Saknad", + "LabelMissingParts": "Saknade delar", + "LabelMore": "Mer", + "LabelMoreInfo": "Mer information", + "LabelName": "Namn", + "LabelNarrator": "Berättare", + "LabelNarrators": "Berättare", + "LabelNew": "Ny", + "LabelNewestAuthors": "Nyaste författare", + "LabelNewestEpisodes": "Nyaste avsnitt", + "LabelNewPassword": "Nytt lösenord", + "LabelNextBackupDate": "Nästa säkerhetskopia datum", + "LabelNextScheduledRun": "Nästa schemalagda körning", + "LabelNoEpisodesSelected": "Inga avsnitt valda", + "LabelNotes": "Anteckningar", + "LabelNotFinished": "Ej avslutad", + "LabelNotificationAppriseURL": "Apprise URL(er)", + "LabelNotificationAvailableVariables": "Tillgängliga variabler", + "LabelNotificationBodyTemplate": "Kroppsmall", + "LabelNotificationEvent": "Aviseringshändelse", + "LabelNotificationsMaxFailedAttempts": "Max antal misslyckade försök", + "LabelNotificationsMaxFailedAttemptsHelp": "Aviseringar inaktiveras när de misslyckas med att skickas så många gånger", + "LabelNotificationsMaxQueueSize": "Max köstorlek för aviseringsevenemang", + "LabelNotificationsMaxQueueSizeHelp": "Evenemang är begränsade till att utlösa ett per sekund. Evenemang kommer att ignoreras om kön är full. Detta förhindrar aviseringsspam.", + "LabelNotificationTitleTemplate": "Titelsmall", + "LabelNotStarted": "Inte påbörjad", + "LabelNumberOfBooks": "Antal böcker", + "LabelNumberOfEpisodes": "Antal avsnitt", + "LabelOpenRSSFeed": "Öppna RSS-flöde", + "LabelOverwrite": "Skriv över", + "LabelPassword": "Lösenord", + "LabelPath": "Sökväg", + "LabelPermissionsAccessAllLibraries": "Kan komma åt alla bibliotek", + "LabelPermissionsAccessAllTags": "Kan komma åt alla taggar", + "LabelPermissionsAccessExplicitContent": "Kan komma åt explicit innehåll", + "LabelPermissionsDelete": "Kan radera", + "LabelPermissionsDownload": "Kan ladda ner", + "LabelPermissionsUpdate": "Kan uppdatera", + "LabelPermissionsUpload": "Kan ladda upp", + "LabelPhotoPathURL": "Bildsökväg/URL", + "LabelPlaylists": "Spellistor", + "LabelPlayMethod": "Spelläge", + "LabelPodcast": "Podcast", + "LabelPodcasts": "Podcasts", + "LabelPodcastType": "Podcasttyp", + "LabelPort": "Port", + "LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)", + "LabelPreventIndexing": "Förhindra att ditt flöde indexeras av iTunes och Google-podcastsökmotorer", + "LabelPrimaryEbook": "Primär e-bok", + "LabelProgress": "Framsteg", + "LabelProvider": "Leverantör", + "LabelPubDate": "Publiceringsdatum", + "LabelPublisher": "Utgivare", + "LabelPublishYear": "Publiceringsår", + "LabelRead": "Läst", + "LabelReadAgain": "Läs igen", + "LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg", + "LabelRecentlyAdded": "Nyligen tillagd", + "LabelRecentSeries": "Senaste serier", + "LabelRecommended": "Rekommenderad", + "LabelRegion": "Region", + "LabelReleaseDate": "Utgivningsdatum", + "LabelRemoveCover": "Ta bort omslag", + "LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post", + "LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn", + "LabelRSSFeedOpen": "Öppna RSS-flöde", + "LabelRSSFeedPreventIndexing": "Förhindra indexering", + "LabelRSSFeedSlug": "RSS-flödesslag", + "LabelRSSFeedURL": "RSS-flöde URL", + "LabelSearchTerm": "Sökterm", + "LabelSearchTitle": "Sök titel", + "LabelSearchTitleOrASIN": "Sök titel eller ASIN", + "LabelSeason": "Säsong", + "LabelSelectAllEpisodes": "Välj alla avsnitt", + "LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas", + "LabelSelectUsers": "Välj användare", + "LabelSendEbookToDevice": "Skicka e-bok till...", + "LabelSequence": "Sekvens", + "LabelSeries": "Serie", + "LabelSeriesName": "Serienamn", + "LabelSeriesProgress": "Serieframsteg", + "LabelSetEbookAsPrimary": "Ange som primär", + "LabelSetEbookAsSupplementary": "Ange som kompletterande", + "LabelSettingsAudiobooksOnly": "Endast ljudböcker", + "LabelSettingsAudiobooksOnlyHelp": "Aktivera detta alternativ kommer att ignorera e-boksfiler om de inte finns inom en ljudboksmapp, i vilket fall de kommer att anges som kompletterande e-böcker", + "LabelSettingsBookshelfViewHelp": "Skeumorfisk design med trähyllor", + "LabelSettingsChromecastSupport": "Chromecast-stöd", + "LabelSettingsDateFormat": "Datumformat", + "LabelSettingsDisableWatcher": "Inaktivera Watcher", + "LabelSettingsDisableWatcherForLibrary": "Inaktivera mappbevakning för bibliotek", + "LabelSettingsDisableWatcherHelp": "Inaktiverar automatiskt lägga till/uppdatera objekt när filändringar upptäcks. *Kräver omstart av servern", + "LabelSettingsEnableWatcher": "Aktivera Watcher", + "LabelSettingsEnableWatcherForLibrary": "Aktivera mappbevakning för bibliotek", + "LabelSettingsEnableWatcherHelp": "Aktiverar automatiskt lägga till/uppdatera objekt när filändringar upptäcks. *Kräver omstart av servern", + "LabelSettingsExperimentalFeatures": "Experimentella funktioner", + "LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.", + "LabelSettingsFindCovers": "Hitta omslag", + "LabelSettingsFindCoversHelp": "Om din ljudbok inte har ett inbäddat omslag eller en omslagsbild i mappen kommer skannern att försöka hitta ett omslag.<br>Observera: Detta kommer att förlänga skannningstiden", + "LabelSettingsHideSingleBookSeries": "Dölj enboksserier", + "LabelSettingsHideSingleBookSeriesHelp": "Serier som har en enda bok kommer att döljas från seriesidan och hyllsidan på startsidan.", + "LabelSettingsHomePageBookshelfView": "Startsida använd bokhyllvy", + "LabelSettingsLibraryBookshelfView": "Bibliotek använd bokhyllvy", + "LabelSettingsParseSubtitles": "Analysera undertexter", + "LabelSettingsParseSubtitlesHelp": "Extrahera undertexter från mappnamn för ljudböcker.<br>Undertext måste vara åtskilda av \" - \"<br>t.ex. \"Boktitel - En undertitel här\" har undertiteln \"En undertitel här\"", + "LabelSettingsPreferMatchedMetadata": "Föredra matchad metadata", + "LabelSettingsPreferMatchedMetadataHelp": "Matchad data kommer att åsidosätta objektdetaljer vid snabbmatchning. Som standard kommer snabbmatchning endast att fylla i saknade detaljer.", + "LabelSettingsSkipMatchingBooksWithASIN": "Hoppa över matchande böcker med ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Hoppa över matchande böcker med ISBN", + "LabelSettingsSortingIgnorePrefixes": "Ignorera prefix vid sortering", + "LabelSettingsSortingIgnorePrefixesHelp": "t.ex. för prefixet \"the\" kommer boktiteln \"The Book Title\" att sorteras som \"Book Title, The\"", + "LabelSettingsSquareBookCovers": "Använd fyrkantiga bokomslag", + "LabelSettingsSquareBookCoversHelp": "Föredrar att använda fyrkantiga omslag över standard 1.6:1 bokomslag", + "LabelSettingsStoreCoversWithItem": "Lagra omslag med objekt", + "LabelSettingsStoreCoversWithItemHelp": "Som standard lagras omslag i /metadata/items, att aktivera detta alternativ kommer att lagra omslag i din biblioteksmapp. Endast en fil med namnet \"cover\" kommer att behållas", + "LabelSettingsStoreMetadataWithItem": "Lagra metadata med objekt", + "LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i /metadata/items, att aktivera detta alternativ kommer att lagra metadatafiler i dina biblioteksmappar", + "LabelSettingsTimeFormat": "Tidsformat", + "LabelShowAll": "Visa alla", + "LabelSize": "Storlek", + "LabelSleepTimer": "Sleeptimer", + "LabelSlug": "Slug", + "LabelStart": "Start", + "LabelStarted": "Startad", + "LabelStartedAt": "Startad vid", + "LabelStartTime": "Starttid", + "LabelStatsAudioTracks": "Ljudspår", + "LabelStatsAuthors": "Författare", + "LabelStatsBestDay": "Bästa dag", + "LabelStatsDailyAverage": "Dagligt genomsnitt", + "LabelStatsDays": "Dagar", + "LabelStatsDaysListened": "Dagar lyssnade", + "LabelStatsHours": "Timmar", + "LabelStatsInARow": "i rad", + "LabelStatsItemsFinished": "Objekt avslutade", + "LabelStatsItemsInLibrary": "Objekt i biblioteket", + "LabelStatsMinutes": "minuter", + "LabelStatsMinutesListening": "Minuter av lyssnande", + "LabelStatsOverallDays": "Totalt antal dagar", + "LabelStatsOverallHours": "Totalt antal timmar", + "LabelStatsWeekListening": "Veckans lyssnande", + "LabelSubtitle": "Underrubrik", + "LabelSupportedFileTypes": "Stödda filtyper", + "LabelTag": "Tagg", + "LabelTags": "Taggar", + "LabelTagsAccessibleToUser": "Taggar tillgängliga för användaren", + "LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren", + "LabelTasks": "Körande uppgifter", + "LabelTheme": "Tema", + "LabelThemeDark": "Mörkt", + "LabelThemeLight": "Ljust", + "LabelTimeBase": "Tidsbas", + "LabelTimeListened": "Tid lyssnad", + "LabelTimeListenedToday": "Tid lyssnad idag", + "LabelTimeRemaining": "{0} kvar", + "LabelTimeToShift": "Tid att skifta i sekunder", + "LabelTitle": "Titel", + "LabelToolsEmbedMetadata": "Bädda in metadata", + "LabelToolsEmbedMetadataDescription": "Bädda in metadata i ljudfiler, inklusive omslagsbild och kapitel.", + "LabelToolsMakeM4b": "Skapa M4B ljudbok", + "LabelToolsMakeM4bDescription": "Skapa en .M4B ljudboksfil med inbäddad metadata, omslagsbild och kapitel.", + "LabelToolsSplitM4b": "Dela M4B till MP3-filer", + "LabelToolsSplitM4bDescription": "Skapa MP3-filer från en M4B fil uppdelad i kapitel med inbäddad metadata, omslagsbild och kapitel.", + "LabelTotalDuration": "Total varaktighet", + "LabelTotalTimeListened": "Total tid lyssnad", + "LabelTrackFromFilename": "Spår från filnamn", + "LabelTrackFromMetadata": "Spår från metadata", + "LabelTracks": "Spår", + "LabelTracksMultiTrack": "Flerspårigt", + "LabelTracksNone": "Inga spår", + "LabelTracksSingleTrack": "Enspårigt", + "LabelType": "Typ", + "LabelUnabridged": "Oavkortad", + "LabelUnknown": "Okänd", + "LabelUpdateCover": "Uppdatera omslag", + "LabelUpdateCoverHelp": "Tillåt överskrivning av befintliga omslag för de valda böckerna när en matchning hittas", + "LabelUpdatedAt": "Uppdaterad vid", + "LabelUpdateDetails": "Uppdatera detaljer", + "LabelUpdateDetailsHelp": "Tillåt överskrivning av befintliga detaljer för de valda böckerna när en matchning hittas", + "LabelUploaderDragAndDrop": "Dra och släpp filer eller mappar", + "LabelUploaderDropFiles": "Släpp filer", + "LabelUseChapterTrack": "Använd kapitelspår", + "LabelUseFullTrack": "Använd hela spåret", + "LabelUser": "Användare", + "LabelUsername": "Användarnamn", + "LabelValue": "Värde", + "LabelVersion": "Version", + "LabelViewBookmarks": "Visa bokmärken", + "LabelViewChapters": "Visa kapitel", + "LabelViewQueue": "Visa spellista", + "LabelVolume": "Volym", + "LabelWeekdaysToRun": "Vardagar att köra", + "LabelYourAudiobookDuration": "Din ljudboks varaktighet", + "LabelYourBookmarks": "Dina bokmärken", + "LabelYourPlaylists": "Dina spellistor", + "LabelYourProgress": "Din framsteg", + "MessageAddToPlayerQueue": "Lägg till i spellistan", + "MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> igång eller en API som hanterar dessa begäranden. <br />Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på <code>http://192.168.1.1:8337</code>, bör du ange <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "Säkerhetskopieringar inkluderar användare, användares framsteg, biblioteksföremål, serverinställningar och bilder lagrade i <code>/metadata/items</code> & <code>/metadata/authors</code>. Säkerhetskopieringar inkluderar <strong>inte</strong> några filer lagrade i dina biblioteksmappar.", + "MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.", + "MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar", + "MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna", + "MessageBookshelfNoSeries": "Du har inga serier", + "MessageChapterEndIsAfter": "Kapitelns slut är efter din ljudboks slut", + "MessageChapterErrorFirstNotZero": "Första kapitlet måste börja vid 0", + "MessageChapterErrorStartGteDuration": "Ogiltig starttid måste vara mindre än ljudbokens varaktighet", + "MessageChapterErrorStartLtPrev": "Ogiltig starttid måste vara större än eller lika med tidigare kapitels starttid", + "MessageChapterStartIsAfter": "Kapitlets start är efter din ljudboks slut", + "MessageCheckingCron": "Kontrollerar cron...", + "MessageConfirmCloseFeed": "Är du säker på att du vill stänga detta flöde?", + "MessageConfirmDeleteBackup": "Är du säker på att du vill radera säkerhetskopian för {0}?", + "MessageConfirmDeleteFile": "Detta kommer att radera filen från ditt filsystem. Är du säker?", + "MessageConfirmDeleteLibrary": "Är du säker på att du vill radera biblioteket \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "Detta kommer att radera biblioteksföremålet från databasen och ditt filsystem. Är du säker?", + "MessageConfirmDeleteLibraryItems": "Detta kommer att radera {0} biblioteksföremål från databasen och ditt filsystem. Är du säker?", + "MessageConfirmDeleteSession": "Är du säker på att du vill radera denna session?", + "MessageConfirmForceReScan": "Är du säker på att du vill tvinga omgenomsökning?", + "MessageConfirmMarkAllEpisodesFinished": "Är du säker på att du vill markera alla avsnitt som avslutade?", + "MessageConfirmMarkAllEpisodesNotFinished": "Är du säker på att du vill markera alla avsnitt som inte avslutade?", + "MessageConfirmMarkSeriesFinished": "Är du säker på att du vill markera alla böcker i denna serie som avslutade?", + "MessageConfirmMarkSeriesNotFinished": "Är du säker på att du vill markera alla böcker i denna serie som inte avslutade?", + "MessageConfirmQuickEmbed": "Varning! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler. <br><br>Vill du fortsätta?", + "MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?", + "MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?", + "MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?", + "MessageConfirmRemoveEpisode": "Är du säker på att du vill ta bort avsnittet \"{0}\"?", + "MessageConfirmRemoveEpisodes": "Är du säker på att du vill ta bort {0} avsnitt?", + "MessageConfirmRemoveNarrator": "Är du säker på att du vill ta bort berättaren \"{0}\"?", + "MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?", + "MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på genren \"{0}\" till \"{1}\" för alla objekt?", + "MessageConfirmRenameGenreMergeNote": "Observera: Den här genren finns redan, så de kommer att slås samman.", + "MessageConfirmRenameGenreWarning": "Varning! En liknande genre med annat skrivsätt finns redan \"{0}\".", + "MessageConfirmRenameTag": "Är du säker på att du vill byta namn på taggen \"{0}\" till \"{1}\" för alla objekt?", + "MessageConfirmRenameTagMergeNote": "Observera: Den här taggen finns redan, så de kommer att slås samman.", + "MessageConfirmRenameTagWarning": "Varning! En liknande tagg med annat skrivsätt finns redan \"{0}\".", + "MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra omgenomsökning för {0} objekt?", + "MessageConfirmSendEbookToDevice": "Är du säker på att du vill skicka {0} e-bok \"{1}\" till enheten \"{2}\"?", + "MessageDownloadingEpisode": "Laddar ner avsnitt", + "MessageDragFilesIntoTrackOrder": "Dra filer till rätt spårordning", + "MessageEmbedFinished": "Inbäddning klar!", + "MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning", + "MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}", + "MessageFetching": "Hämtar...", + "MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.", + "MessageImportantNotice": "Viktig meddelande!", + "MessageInsertChapterBelow": "Infoga kapitel nedanför", + "MessageItemsSelected": "{0} Objekt markerade", + "MessageItemsUpdated": "{0} Objekt uppdaterade", + "MessageJoinUsOn": "Anslut dig till oss på", + "MessageListeningSessionsInTheLastYear": "{0} lyssningssessioner det senaste året", + "MessageLoading": "Laddar...", + "MessageLoadingFolders": "Laddar mappar...", + "MessageM4BFailed": "M4B misslyckades!", + "MessageM4BFinished": "M4B klar!", + "MessageMapChapterTitles": "Kartlägg kapitelrubriker till dina befintliga ljudbokskapitel utan att justera tidstämplar", + "MessageMarkAllEpisodesFinished": "Markera alla avsnitt som avslutade", + "MessageMarkAllEpisodesNotFinished": "Markera alla avsnitt som inte avslutade", + "MessageMarkAsFinished": "Markera som avslutad", + "MessageMarkAsNotFinished": "Markera som inte avslutad", + "MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från den valda sökleverantören och fylla i tomma detaljer och omslagskonst. Överskriver inte detaljer.", + "MessageNoAudioTracks": "Inga ljudspår", + "MessageNoAuthors": "Inga författare", + "MessageNoBackups": "Inga säkerhetskopior", + "MessageNoBookmarks": "Inga bokmärken", + "MessageNoChapters": "Inga kapitel", + "MessageNoCollections": "Inga samlingar", + "MessageNoCoversFound": "Inga omslag hittade", + "MessageNoDescription": "Ingen beskrivning", + "MessageNoDownloadsInProgress": "Inga nedladdningar pågår för närvarande", + "MessageNoDownloadsQueued": "Inga nedladdningar i kö", + "MessageNoEpisodeMatchesFound": "Inga matchande avsnitt hittades", + "MessageNoEpisodes": "Inga avsnitt", + "MessageNoFoldersAvailable": "Inga mappar tillgängliga", + "MessageNoGenres": "Inga genrer", + "MessageNoIssues": "Inga problem", + "MessageNoItems": "Inga objekt", + "MessageNoItemsFound": "Inga objekt hittades", + "MessageNoListeningSessions": "Inga lyssningssessioner", + "MessageNoLogs": "Inga loggar", + "MessageNoMediaProgress": "Ingen medieförlopp", + "MessageNoNotifications": "Inga aviseringar", + "MessageNoPodcastsFound": "Inga podcasts hittade", + "MessageNoResults": "Inga resultat", + "MessageNoSearchResultsFor": "Inga sökresultat för \"{0}\"", + "MessageNoSeries": "Inga serier", + "MessageNoTags": "Inga taggar", + "MessageNoTasksRunning": "Inga pågående uppgifter", + "MessageNotYetImplemented": "Ännu inte implementerad", + "MessageNoUpdateNecessary": "Ingen uppdatering krävs", + "MessageNoUpdatesWereNecessary": "Inga uppdateringar var nödvändiga", + "MessageNoUserPlaylists": "Du har inga spellistor", + "MessageOr": "eller", + "MessagePauseChapter": "Pausa kapiteluppspelning", + "MessagePlayChapter": "Lyssna på kapitlets början", + "MessagePlaylistCreateFromCollection": "Skapa spellista från samling", + "MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning", + "MessageQuickMatchDescription": "Fyll tomma objektdetaljer och omslag med första matchningsresultat från '{0}'. Överskriver inte detaljer om inte serverinställningen 'Föredra matchad metadata' är aktiverad.", + "MessageRemoveChapter": "Ta bort kapitel", + "MessageRemoveEpisodes": "Ta bort {0} avsnitt", + "MessageRemoveFromPlayerQueue": "Ta bort från spellistan", + "MessageRemoveUserWarning": "Är du säker på att du vill radera användaren \"{0}\" permanent?", + "MessageReportBugsAndContribute": "Rapportera buggar, begär funktioner och bidra på", + "MessageResetChaptersConfirm": "Är du säker på att du vill återställa kapitel och ångra ändringarna du gjort?", + "MessageRestoreBackupConfirm": "Är du säker på att du vill återställa säkerhetskopian som skapades den", + "MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.<br /><br />Säkerhetskopior ändrar inte några filer i dina biblioteksmappar. Om du har aktiverat serverinställningar för att lagra omslagskonst och metadata i dina biblioteksmappar säkerhetskopieras eller skrivs de inte över.<br /><br />Alla klienter som använder din server kommer att uppdateras automatiskt.", + "MessageSearchResultsFor": "Sökresultat för", + "MessageServerCouldNotBeReached": "Servern kunde inte nås", + "MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn", + "MessageStartPlaybackAtTime": "Starta uppspelning för \"{0}\" kl. {1}?", + "MessageThinking": "Tänker...", + "MessageUploaderItemFailed": "Misslyckades med att ladda upp", + "MessageUploaderItemSuccess": "Uppladdning lyckades!", + "MessageUploading": "Laddar upp...", + "MessageValidCronExpression": "Giltigt cron-uttryck", + "MessageWatcherIsDisabledGlobally": "Vakten är inaktiverad globalt i serverinställningarna", + "MessageXLibraryIsEmpty": "{0} biblioteket är tomt!", + "MessageYourAudiobookDurationIsLonger": "Varaktigheten på din ljudbok är längre än den hittade varaktigheten", + "MessageYourAudiobookDurationIsShorter": "Varaktigheten på din ljudbok är kortare än den hittade varaktigheten", + "NoteChangeRootPassword": "Rotanvändaren är den enda användaren som kan ha ett tomt lösenord", + "NoteChapterEditorTimes": "Obs: Starttiden för första kapitlet måste förbli 0:00 och starttiden för det sista kapitlet får inte överstiga ljudbokens varaktighet.", + "NoteFolderPicker": "Obs: Mappar som redan är kartlagda kommer inte att visas", + "NoteFolderPickerDebian": "Obs: Mappväljaren för Debian-installationen är inte fullständigt implementerad. Du bör ange sökvägen till ditt bibliotek direkt.", + "NoteRSSFeedPodcastAppsHttps": "Varning: De flesta podcastappar kräver att RSS-flödets URL används med HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "Varning: 1 eller flera av dina avsnitt har inte ett publiceringsdatum. Vissa podcastappar kräver detta.", + "NoteUploaderFoldersWithMediaFiles": "Mappar med mediefiler hanteras som separata biblioteksobjekt.", + "NoteUploaderOnlyAudioFiles": "Om du bara laddar upp ljudfiler kommer varje ljudfil att hanteras som en separat ljudbok.", + "NoteUploaderUnsupportedFiles": "Oaccepterade filer ignoreras. När du väljer eller släpper en mapp ignoreras andra filer som inte finns i ett objektmapp.", + "PlaceholderNewCollection": "Nytt samlingsnamn", + "PlaceholderNewFolderPath": "Nytt mappväg", + "PlaceholderNewPlaylist": "Nytt spellistanamn", + "PlaceholderSearch": "Sök...", + "PlaceholderSearchEpisode": "Sök avsnitt...", + "ToastAccountUpdateFailed": "Det gick inte att uppdatera kontot", + "ToastAccountUpdateSuccess": "Kontot uppdaterat", + "ToastAuthorImageRemoveFailed": "Det gick inte att ta bort författarens bild", + "ToastAuthorImageRemoveSuccess": "Författarens bild borttagen", + "ToastAuthorUpdateFailed": "Det gick inte att uppdatera författaren", + "ToastAuthorUpdateMerged": "Författaren sammanslagen", + "ToastAuthorUpdateSuccess": "Författaren uppdaterad", + "ToastAuthorUpdateSuccessNoImageFound": "Författaren uppdaterad (ingen bild hittad)", + "ToastBackupCreateFailed": "Det gick inte att skapa en säkerhetskopia", + "ToastBackupCreateSuccess": "Säkerhetskopia skapad", + "ToastBackupDeleteFailed": "Det gick inte att ta bort säkerhetskopian", + "ToastBackupDeleteSuccess": "Säkerhetskopan borttagen", + "ToastBackupRestoreFailed": "Det gick inte att återställa säkerhetskopan", + "ToastBackupUploadFailed": "Det gick inte att ladda upp säkerhetskopan", + "ToastBackupUploadSuccess": "Säkerhetskopan uppladdad", + "ToastBatchUpdateFailed": "Batchuppdateringen misslyckades", + "ToastBatchUpdateSuccess": "Batchuppdateringen lyckades", + "ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket", + "ToastBookmarkCreateSuccess": "Bokmärket tillagt", + "ToastBookmarkRemoveFailed": "Det gick inte att ta bort bokmärket", + "ToastBookmarkRemoveSuccess": "Bokmärket borttaget", + "ToastBookmarkUpdateFailed": "Det gick inte att uppdatera bokmärket", + "ToastBookmarkUpdateSuccess": "Bokmärket uppdaterat", + "ToastChaptersHaveErrors": "Kapitlen har fel", + "ToastChaptersMustHaveTitles": "Kapitel måste ha titlar", + "ToastCollectionItemsRemoveFailed": "Det gick inte att ta bort objekt från samlingen", + "ToastCollectionItemsRemoveSuccess": "Objekt borttagna från samlingen", + "ToastCollectionRemoveFailed": "Det gick inte att ta bort samlingen", + "ToastCollectionRemoveSuccess": "Samlingen borttagen", + "ToastCollectionUpdateFailed": "Det gick inte att uppdatera samlingen", + "ToastCollectionUpdateSuccess": "Samlingen uppdaterad", + "ToastItemCoverUpdateFailed": "Det gick inte att uppdatera objektets omslag", + "ToastItemCoverUpdateSuccess": "Objektets omslag uppdaterat", + "ToastItemDetailsUpdateFailed": "Det gick inte att uppdatera objektdetaljerna", + "ToastItemDetailsUpdateSuccess": "Objektdetaljer uppdaterade", + "ToastItemDetailsUpdateUnneeded": "Inga uppdateringar behövs för objektdetaljerna", + "ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera som färdig", + "ToastItemMarkedAsFinishedSuccess": "Objekt markerat som färdig", + "ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera som ej färdig", + "ToastItemMarkedAsNotFinishedSuccess": "Objekt markerat som ej färdig", + "ToastLibraryCreateFailed": "Det gick inte att skapa biblioteket", + "ToastLibraryCreateSuccess": "Biblioteket \"{0}\" skapat", + "ToastLibraryDeleteFailed": "Det gick inte att ta bort biblioteket", + "ToastLibraryDeleteSuccess": "Biblioteket borttaget", + "ToastLibraryScanFailedToStart": "Misslyckades med att starta skanningen", + "ToastLibraryScanStarted": "Skanning av biblioteket påbörjad", + "ToastLibraryUpdateFailed": "Det gick inte att uppdatera biblioteket", + "ToastLibraryUpdateSuccess": "Biblioteket \"{0}\" uppdaterat", + "ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan", + "ToastPlaylistCreateSuccess": "Spellistan skapad", + "ToastPlaylistRemoveFailed": "Det gick inte att ta bort spellistan", + "ToastPlaylistRemoveSuccess": "Spellistan borttagen", + "ToastPlaylistUpdateFailed": "Det gick inte att uppdatera spellistan", + "ToastPlaylistUpdateSuccess": "Spellistan uppdaterad", + "ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten", + "ToastPodcastCreateSuccess": "Podcasten skapad framgångsrikt", + "ToastRemoveItemFromCollectionFailed": "Misslyckades med att ta bort objektet från samlingen", + "ToastRemoveItemFromCollectionSuccess": "Objektet borttaget från samlingen", + "ToastRSSFeedCloseFailed": "Misslyckades med att stänga RSS-flödet", + "ToastRSSFeedCloseSuccess": "RSS-flödet stängt", + "ToastSendEbookToDeviceFailed": "Misslyckades med att skicka e-boken till enheten", + "ToastSendEbookToDeviceSuccess": "E-boken skickad till enheten \"{0}\"", + "ToastSeriesUpdateFailed": "Serieuppdateringen misslyckades", + "ToastSeriesUpdateSuccess": "Serieuppdateringen lyckades", + "ToastSessionDeleteFailed": "Misslyckades med att ta bort sessionen", + "ToastSessionDeleteSuccess": "Sessionen borttagen", + "ToastSocketConnected": "Socket ansluten", + "ToastSocketDisconnected": "Socket frånkopplad", + "ToastSocketFailedToConnect": "Socket misslyckades med att ansluta", + "ToastUserDeleteFailed": "Misslyckades med att ta bort användaren", + "ToastUserDeleteSuccess": "Användaren borttagen" + } From 61e05e92a8e133c19b557b166597fc3771a3ef3a Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 5 Nov 2023 10:16:40 -0600 Subject: [PATCH 123/208] Add Swedish language option --- client/plugins/i18n.js | 1 + client/strings/{se.json => sv.json} | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename client/strings/{se.json => sv.json} (99%) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 9a7eb02e8..ea6a06db7 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -18,6 +18,7 @@ const languageCodeMap = { 'no': { label: 'Norsk', dateFnsLocale: 'no' }, 'pl': { label: 'Polski', dateFnsLocale: 'pl' }, 'ru': { label: 'Русский', dateFnsLocale: 'ru' }, + 'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, } Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { diff --git a/client/strings/se.json b/client/strings/sv.json similarity index 99% rename from client/strings/se.json rename to client/strings/sv.json index f0580847c..23d489d05 100644 --- a/client/strings/se.json +++ b/client/strings/sv.json @@ -726,4 +726,4 @@ "ToastSocketFailedToConnect": "Socket misslyckades med att ansluta", "ToastUserDeleteFailed": "Misslyckades med att ta bort användaren", "ToastUserDeleteSuccess": "Användaren borttagen" - } +} \ No newline at end of file From 309ef807abb21e6eb25af525a9670ad1c3352a8a Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 5 Nov 2023 12:37:05 -0600 Subject: [PATCH 124/208] Update /auth/openid endpoint to work with PKCE from mobile Co-authored-by: Denis Arnst <git@sapd.eu> --- server/Auth.js | 82 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index fa5020a0c..a04f9ac4b 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -74,7 +74,7 @@ class Auth { client_id: global.ServerSettings.authOpenIDClientID, client_secret: global.ServerSettings.authOpenIDClientSecret }) - const openIdClientStrategy = new OpenIDClient.Strategy({ + passport.use('openid-client', new OpenIDClient.Strategy({ client: openIdClient, params: { redirect_uri: '/auth/openid/callback', @@ -99,12 +99,7 @@ class Auth { // permit login return done(null, user) - }) - // The strategy name is set to the issuer hostname by default but didnt' see a way to override this - // @see https://github.com/panva/node-openid-client/blob/a84d022f195f82ca1c97f8f6b2567ebcef8738c3/lib/passport_strategy.js#L75 - openIdClientStrategy.name = 'openid-client' - - passport.use(openIdClientStrategy) + })) } // Load the JwtStrategy (always) -> for bearer token auth @@ -235,16 +230,77 @@ class Auth { // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { - // This is a (temporary?) hack to not have to get the full redirect URL from the user - // it uses the URL made in this request and adds the relative URL /auth/openid/callback - const strategy = passport._strategy('openid-client') - strategy._params.redirect_uri = new URL(`${req.protocol}://${req.get('host')}/auth/openid/callback`).toString() + // helper function from openid-client + function pick(object, ...paths) { + const obj = {} + for (const path of paths) { + if (object[path] !== undefined) { + obj[path] = object[path] + } + } + return obj + } + // Get the OIDC client from the strategy + // We need to call the client manually, because the strategy does not support forwarding the code challenge + // for API or mobile clients + const oidcStrategy = passport._strategy('openid-client') + oidcStrategy._params.redirect_uri = new URL(`${req.protocol}://${req.get('host')}/auth/openid/callback`).toString() + const client = oidcStrategy._client + const sessionKey = oidcStrategy._key + + let code_challenge + let code_challenge_method + + // If code_challenge is provided, expect that code_verifier will be handled by the client (mobile app) + // The web frontend of ABS does not need to do a PKCE itself, because it never handles the "code" of the oauth flow + // and as such will not send a code challenge, we will generate then one + if (req.query.code_challenge) { + code_challenge = req.query.code_challenge + code_challenge_method = req.query.code_challenge_method || 'S256' + + if (!['S256', 'plain'].includes(code_challenge_method)) { + return res.status(400).send('Invalid code_challenge_method') + } + } else { + // If no code_challenge is provided, assume a web application flow and generate one + const code_verifier = OpenIDClient.generators.codeVerifier() + code_challenge = OpenIDClient.generators.codeChallenge(code_verifier) + code_challenge_method = 'S256' + + // Store the code_verifier in the session for later use in the token exchange + req.session[sessionKey] = { ...req.session[sessionKey], code_verifier } + } + + const params = { + state: OpenIDClient.generators.random(), + // Other params by the passport strategy + ...oidcStrategy._params + } + + if (!params.nonce && params.response_type.includes('id_token')) { + params.nonce = OpenIDClient.generators.random() + } + + req.session[sessionKey] = { + ...req.session[sessionKey], + ...pick(params, 'nonce', 'state', 'max_age', 'response_type') + } + + // Now get the URL to direct to + const authorizationUrl = client.authorizationUrl({ + ...params, + scope: 'openid profile email', + response_type: 'code', + code_challenge, + code_challenge_method, + }) - const auth_func = passport.authenticate('openid-client') // params (isRest, callback) to a cookie that will be send to the client this.paramsToCookies(req, res) - auth_func(req, res, next) + + // Redirect the user agent (browser) to the authorization URL + res.redirect(authorizationUrl) }) // openid strategy callback route (this receives the token from the configured openid login provider) From c17540e191b202a53c4ecf580f0e527343fd9eca Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 5 Nov 2023 12:43:34 -0600 Subject: [PATCH 125/208] Add app and serverVersion properties to response from /status --- server/Server.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/Server.js b/server/Server.js index 6c6a17b08..df6c90034 100644 --- a/server/Server.js +++ b/server/Server.js @@ -228,6 +228,8 @@ class Server { // status check for client to see if server has been initialized // server has been initialized if a root user exists const payload = { + app: 'audiobookshelf', + serverVersion: version, isInit: Database.hasRootUser, language: Database.serverSettings.language, authMethods: Database.serverSettings.authActiveAuthMethods, From f840aa80f8989a5d64b0a8e5935866906132a316 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 5 Nov 2023 14:11:37 -0600 Subject: [PATCH 126/208] Add button to populate openid URLs using the issuer URL --- client/pages/config/authentication.vue | 43 +++++++++++++++++++++++++- server/Auth.js | 27 ++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 13867ef3a..7cedfd25a 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -15,7 +15,17 @@ <div class="overflow-hidden"> <transition name="slide"> <div v-if="enableOpenIDAuth" class="flex flex-wrap pt-4"> - <ui-text-input-with-label ref="issuerUrl" v-model="newAuthSettings.authOpenIDIssuerURL" :disabled="savingSettings" :label="'Issuer URL'" class="mb-2" /> + <div class="w-full flex items-center mb-2"> + <div class="flex-grow"> + <ui-text-input-with-label ref="issuerUrl" v-model="newAuthSettings.authOpenIDIssuerURL" :disabled="savingSettings" :label="'Issuer URL'" /> + </div> + <div class="w-36 mx-1 mt-[1.375rem]"> + <ui-btn class="h-[2.375rem] text-sm inline-flex items-center justify-center w-full" type="button" :padding-y="0" :padding-x="4" @click.stop="autoPopulateOIDCClick"> + <span class="material-icons text-base">auto_fix_high</span> + <span class="whitespace-nowrap break-keep pl-1">Auto-populate</span></ui-btn + > + </div> + </div> <ui-text-input-with-label ref="authorizationUrl" v-model="newAuthSettings.authOpenIDAuthorizationURL" :disabled="savingSettings" :label="'Authorize URL'" class="mb-2" /> @@ -83,6 +93,37 @@ export default { } }, methods: { + autoPopulateOIDCClick() { + if (!this.newAuthSettings.authOpenIDIssuerURL) { + this.$toast.error('Issuer URL required') + return + } + // Remove trailing slash + let issuerUrl = this.newAuthSettings.authOpenIDIssuerURL + if (issuerUrl.endsWith('/')) issuerUrl = issuerUrl.slice(0, -1) + + // If the full config path is on the issuer url then remove it + if (issuerUrl.endsWith('/.well-known/openid-configuration')) { + issuerUrl = issuerUrl.replace('/.well-known/openid-configuration', '') + this.newAuthSettings.authOpenIDIssuerURL = this.newAuthSettings.authOpenIDIssuerURL.replace('/.well-known/openid-configuration', '') + } + + this.$axios + .$get(`/auth/openid/config?issuer=${issuerUrl}`) + .then((data) => { + if (data.issuer) this.newAuthSettings.authOpenIDIssuerURL = data.issuer + if (data.authorization_endpoint) this.newAuthSettings.authOpenIDAuthorizationURL = data.authorization_endpoint + if (data.token_endpoint) this.newAuthSettings.authOpenIDTokenURL = data.token_endpoint + if (data.userinfo_endpoint) this.newAuthSettings.authOpenIDUserInfoURL = data.userinfo_endpoint + if (data.end_session_endpoint) this.newAuthSettings.authOpenIDLogoutURL = data.end_session_endpoint + if (data.jwks_uri) this.newAuthSettings.authOpenIDJwksURL = data.jwks_uri + }) + .catch((error) => { + console.error('Failed to receive data', error) + const errorMsg = error.response?.data || 'Unknown error' + this.$toast.error(errorMsg) + }) + }, validateOpenID() { let isValid = true if (!this.newAuthSettings.authOpenIDIssuerURL) { diff --git a/server/Auth.js b/server/Auth.js index a04f9ac4b..361380f8f 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,3 +1,4 @@ +const axios = require('axios') const passport = require('passport') const bcrypt = require('./libs/bcryptjs') const jwt = require('./libs/jsonwebtoken') @@ -309,6 +310,32 @@ class Auth { // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this)) + /** + * Used to auto-populate the openid URLs in config/authentication + */ + router.get('/auth/openid/config', async (req, res) => { + if (!req.query.issuer) { + return res.status(400).send('Invalid request. Query param \'issuer\' is required') + } + let issuerUrl = req.query.issuer + if (issuerUrl.endsWith('/')) issuerUrl = issuerUrl.slice(0, -1) + + const configUrl = `${issuerUrl}/.well-known/openid-configuration` + axios.get(configUrl).then(({ data }) => { + res.json({ + issuer: data.issuer, + authorization_endpoint: data.authorization_endpoint, + token_endpoint: data.token_endpoint, + userinfo_endpoint: data.userinfo_endpoint, + end_session_endpoint: data.end_session_endpoint, + jwks_uri: data.jwks_uri + }) + }).catch((error) => { + Logger.error(`[Auth] Failed to get openid configuration at "${configUrl}"`, error) + res.status(error.statusCode || 400).send(`${error.code || 'UNKNOWN'}: Failed to get openid configuration`) + }) + }) + // Logout route router.post('/logout', (req, res) => { // TODO: invalidate possible JWTs From 0344e8cf1b90eab6411cbea32d7b3c03c61ef011 Mon Sep 17 00:00:00 2001 From: Brian Austin <brianjaustin@gmail.com> Date: Sun, 5 Nov 2023 19:13:26 -0500 Subject: [PATCH 127/208] Hide collection duration if 0 --- client/components/tables/collection/BookTableRow.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/tables/collection/BookTableRow.vue b/client/components/tables/collection/BookTableRow.vue index 399c429a1..ed216bb32 100644 --- a/client/components/tables/collection/BookTableRow.vue +++ b/client/components/tables/collection/BookTableRow.vue @@ -30,7 +30,7 @@ ><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span> </template> </div> - <p class="text-xs md:text-sm text-gray-400">{{ bookDuration }}</p> + <p class="text-xs md:text-sm text-gray-400" v-if="media.duration > 0">{{ bookDuration }}</p> </div> </div> </div> From ba60fc75814a0cf0bd1cd6ba56a2c602378b69e0 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Mon, 6 Nov 2023 05:33:06 +0000 Subject: [PATCH 128/208] Add tests for TitleCanidates --- server/finders/bookFinder.test.js | 62 +++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 server/finders/bookFinder.test.js diff --git a/server/finders/bookFinder.test.js b/server/finders/bookFinder.test.js new file mode 100644 index 000000000..ab82b0f9b --- /dev/null +++ b/server/finders/bookFinder.test.js @@ -0,0 +1,62 @@ +const bookFinder = require('./BookFinder') + +describe('TitleCandidates with author', () => { + let titleCandidates + + beforeEach(() => { + titleCandidates = new bookFinder.constructor.TitleCandidates('leo tolstoy') + }) + + describe('single add', () => { + it.each([ + ['adds a clean title to candidates', 'anna karenina', ['anna karenina']], + ['lowercases candidate title', 'ANNA KARENINA', ['anna karenina']], + ['removes author name from title', 'anna karenina by leo tolstoy', ['anna karenina']], + ['removes author name title', 'leo tolstoy', []], + ['cleans subtitle from title', 'anna karenina: subtitle', ['anna karenina']], + ['removes "by ..." from title', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], + ['removes bitrate from title', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], + ['removes edition from title 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], + ['removes edition from title 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], + ['removes file-type from title', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']], + ['removes "a novel" from title', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']], + ['removes preceding/trailing numbers from title', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']], + ['does not add empty title', '', []], + ['does not add title with only spaces', ' ', []], + ['adds digit-only title, but not its empty string transformation', '1984', ['1984']], + ])('%s', (_, title, expected) => { + titleCandidates.add(title) + expect(titleCandidates.getCandidates()).toEqual(expected) + }) + }) + + describe('multi add', () => { + it.each([ + ['digits-only candidates get lower priority', ['01', 'anna karenina'], ['anna karenina', '01']], + ['transformed candidates get higher priority', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']], + ['other candidates are ordered by position', ['title1', 'title2'], ['title1', 'title2']], + ['author candidate is removed', ['title1', 'leo tolstoy'], ['title1']], + ])('%s', (_, titles, expected) => { + for (const title of titles) titleCandidates.add(title) + expect(titleCandidates.getCandidates()).toEqual(expected) + }) + }) +}) + +describe('TitleCandidates with no author', () => { + let titleCandidates + + beforeEach(() => { + titleCandidates = new bookFinder.constructor.TitleCandidates('') + }) + + describe('single add', () => { + it.each([ + ['does not removes author name', 'leo tolstoy', ['leo tolstoy']], + ])('%s', (_, title, expected) => { + titleCandidates.add(title) + expect(titleCandidates.getCandidates()).toEqual(expected) + }) + }) +}) + From aad6402fdbfd2df3df524b17aefc2df34c089cb6 Mon Sep 17 00:00:00 2001 From: advplyr <dev@advplyr.com> Date: Mon, 6 Nov 2023 16:18:35 -0600 Subject: [PATCH 129/208] Update client/components/tables/collection/BookTableRow.vue --- client/components/tables/collection/BookTableRow.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/tables/collection/BookTableRow.vue b/client/components/tables/collection/BookTableRow.vue index ed216bb32..110edcb96 100644 --- a/client/components/tables/collection/BookTableRow.vue +++ b/client/components/tables/collection/BookTableRow.vue @@ -30,7 +30,7 @@ ><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span> </template> </div> - <p class="text-xs md:text-sm text-gray-400" v-if="media.duration > 0">{{ bookDuration }}</p> + <p v-if="media.duration" class="text-xs md:text-sm text-gray-400">{{ bookDuration }}</p> </div> </div> </div> From 59a428d549e6d115875a01ca59c5905fe41dbc87 Mon Sep 17 00:00:00 2001 From: Dr-Blank <64108942+Dr-Blank@users.noreply.github.com> Date: Mon, 6 Nov 2023 18:10:57 -0500 Subject: [PATCH 130/208] more gu translations --- client/strings/gu.json | 137 +++++++++++++++++++---------------------- 1 file changed, 65 insertions(+), 72 deletions(-) diff --git a/client/strings/gu.json b/client/strings/gu.json index d71c9f176..9a1057224 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -1,10 +1,10 @@ { "ButtonAdd": "ઉમેરો", "ButtonAddChapters": "પ્રકરણો ઉમેરો", - "ButtonAddDevice": "Add Device", - "ButtonAddLibrary": "Add Library", + "ButtonAddDevice": "ઉપકરણ ઉમેરો", + "ButtonAddLibrary": "પુસ્તકાલય ઉમેરો", "ButtonAddPodcasts": "પોડકાસ્ટ ઉમેરો", - "ButtonAddUser": "Add User", + "ButtonAddUser": "વપરાશકર્તા ઉમેરો", "ButtonAddYourFirstLibrary": "તમારી પ્રથમ પુસ્તકાલય ઉમેરો", "ButtonApply": "લાગુ કરો", "ButtonApplyChapters": "પ્રકરણો લાગુ કરો", @@ -58,11 +58,11 @@ "ButtonRemoveAll": "બધું કાઢી નાખો", "ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો", "ButtonRemoveFromContinueListening": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો", - "ButtonRemoveFromContinueReading": "Remove from Continue Reading", + "ButtonRemoveFromContinueReading": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો", "ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો", "ButtonReScan": "ફરીથી સ્કેન કરો", "ButtonReset": "રીસેટ કરો", - "ButtonResetToDefault": "Reset to default", + "ButtonResetToDefault": "ડિફોલ્ટ પર રીસેટ કરો", "ButtonRestore": "પુનઃસ્થાપિત કરો", "ButtonSave": "સાચવો", "ButtonSaveAndClose": "સાચવો અને બંધ કરો", @@ -78,7 +78,7 @@ "ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો", "ButtonStartMetadataEmbed": "મેટાડેટા એમ્બેડ શરૂ કરો", "ButtonSubmit": "સબમિટ કરો", - "ButtonTest": "Test", + "ButtonTest": "પરખ કરો", "ButtonUpload": "અપલોડ કરો", "ButtonUploadBackup": "બેકઅપ અપલોડ કરો", "ButtonUploadCover": "કવર અપલોડ કરો", @@ -90,72 +90,65 @@ "HeaderAccount": "એકાઉન્ટ", "HeaderAdvanced": "અડ્વાન્સડ", "HeaderAppriseNotificationSettings": "Apprise સૂચના સેટિંગ્સ", - "HeaderAudiobookTools": "Audiobook File Management Tools", - "HeaderAudioTracks": "Audio Tracks", - "HeaderBackups": "Backups", - "HeaderChangePassword": "Change Password", - "HeaderChapters": "Chapters", - "HeaderChooseAFolder": "Choose a Folder", - "HeaderCollection": "Collection", - "HeaderCollectionItems": "Collection Items", - "HeaderCover": "Cover", - "HeaderCurrentDownloads": "Current Downloads", - "HeaderDetails": "Details", - "HeaderDownloadQueue": "Download Queue", - "HeaderEbookFiles": "Ebook Files", - "HeaderEmail": "Email", - "HeaderEmailSettings": "Email Settings", - "HeaderEpisodes": "Episodes", - "HeaderEreaderDevices": "Ereader Devices", - "HeaderEreaderSettings": "Ereader Settings", - "HeaderFiles": "Files", - "HeaderFindChapters": "Find Chapters", - "HeaderIgnoredFiles": "Ignored Files", - "HeaderItemFiles": "Item Files", - "HeaderItemMetadataUtils": "Item Metadata Utils", - "HeaderLastListeningSession": "Last Listening Session", - "HeaderLatestEpisodes": "Latest episodes", - "HeaderLibraries": "Libraries", - "HeaderLibraryFiles": "Library Files", - "HeaderLibraryStats": "Library Stats", - "HeaderListeningSessions": "Listening Sessions", - "HeaderListeningStats": "Listening Stats", - "HeaderLogin": "Login", - "HeaderLogs": "Logs", - "HeaderManageGenres": "Manage Genres", - "HeaderManageTags": "Manage Tags", - "HeaderMapDetails": "Map details", - "HeaderMatch": "Match", - "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", - "HeaderMetadataToEmbed": "Metadata to embed", - "HeaderNewAccount": "New Account", - "HeaderNewLibrary": "New Library", - "HeaderNotifications": "Notifications", - "HeaderOpenRSSFeed": "Open RSS Feed", - "HeaderOtherFiles": "Other Files", - "HeaderPermissions": "Permissions", - "HeaderPlayerQueue": "Player Queue", - "HeaderPlaylist": "Playlist", - "HeaderPlaylistItems": "Playlist Items", - "HeaderPodcastsToAdd": "Podcasts to Add", - "HeaderPreviewCover": "Preview Cover", - "HeaderRemoveEpisode": "Remove Episode", - "HeaderRemoveEpisodes": "Remove {0} Episodes", - "HeaderRSSFeedGeneral": "RSS Details", - "HeaderRSSFeedIsOpen": "RSS Feed is Open", - "HeaderRSSFeeds": "RSS Feeds", - "HeaderSavedMediaProgress": "Saved Media Progress", - "HeaderSchedule": "Schedule", - "HeaderScheduleLibraryScans": "Schedule Automatic Library Scans", - "HeaderSession": "Session", - "HeaderSetBackupSchedule": "Set Backup Schedule", - "HeaderSettings": "Settings", - "HeaderSettingsDisplay": "Display", - "HeaderSettingsExperimental": "Experimental Features", - "HeaderSettingsGeneral": "General", - "HeaderSettingsScanner": "Scanner", - "HeaderSleepTimer": "Sleep Timer", - "HeaderStatsLargestItems": "Largest Items", + "HeaderAudiobookTools": "ઓડિયોબુક ફાઇલ વ્યવસ્થાપન ટૂલ્સ", + "HeaderAudioTracks": "ઓડિયો ટ્રેક્સ", + "HeaderBackups": "બેકઅપ્સ", + "HeaderChangePassword": "પાસવર્ડ બદલો", + "HeaderChapters": "પ્રકરણો", + "HeaderChooseAFolder": "ફોલ્ડર પસંદ કરો", + "HeaderCollection": "સંગ્રહ", + "HeaderCollectionItems": "સંગ્રહ વસ્તુઓ", + "HeaderCover": "આવરણ", + "HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ", + "HeaderDetails": "વિગતો", + "HeaderDownloadQueue": "ડાઉનલોડ કતાર", + "HeaderEbookFiles": "ઇબુક ફાઇલો", + "HeaderEmail": "ઈમેલ", + "HeaderEmailSettings": "ઈમેલ સેટિંગ્સ", + "HeaderEpisodes": "એપિસોડ્સ", + "HeaderEreaderDevices": "ઇરીડર ઉપકરણો", + "HeaderEreaderSettings": "ઇરીડર સેટિંગ્સ", + "HeaderFiles": "ફાઇલો", + "HeaderFindChapters": "પ્રકરણો શોધો", + "HeaderIgnoredFiles": "અવગણેલી ફાઇલો", + "HeaderItemFiles": "વાસ્તુ ની ફાઈલો", + "HeaderItemMetadataUtils": "વસ્તુ મેટાડેટા સાધનો", + "HeaderLastListeningSession": "છેલ્લી સાંભળતી સેશન", + "HeaderLatestEpisodes": "નવીનતમ એપિસોડ્સ", + "HeaderLibraries": "પુસ્તકાલયો", + "HeaderLibraryFiles":"પુસ્તકાલય ફાઇલો", + "HeaderLibraryStats": "પુસ્તકાલય આંકડા", + "HeaderListeningSessions": "સાંભળતી સેશન્સ", + "HeaderListeningStats": "સાંભળતી આંકડા", + "HeaderLogin": "લોગિન", + "HeaderLogs": "લોગ્સ", + "HeaderManageGenres": "જાતિઓ મેનેજ કરો", + "HeaderManageTags": "ટેગ્સ મેનેજ કરો", + "HeaderMapDetails": "વિગતો મેપ કરો", + "HeaderMatch": "મેળ ખાતી શોધો", + "HeaderMetadataOrderOfPrecedence": "મેટાડેટા પ્રાધાન્યતાનો ક્રમ", + "HeaderMetadataToEmbed": "એમ્બેડ કરવા માટે મેટાડેટા", + "HeaderNewAccount": "નવું એકાઉન્ટ", + "HeaderNewLibrary": "નવી પુસ્તકાલય", + "HeaderNotifications": "સૂચનાઓ", + "HeaderOpenRSSFeed": "RSS ફીડ ખોલો", + "HeaderOtherFiles": "અન્ય ફાઇલો", + "HeaderPermissions": "પરવાનગીઓ", + "HeaderPlayerQueue": "પ્લેયર કતાર", + "HeaderPlaylist": "પ્લેલિસ્ટ", + "HeaderPlaylistItems": "પ્લેલિસ્ટ ની વસ્તુઓ", + "HeaderPodcastsToAdd": "ઉમેરવા માટે પોડકાસ્ટ્સ", + "HeaderPreviewCover": "પૂર્વાવલોકન કવર", + "HeaderRemoveEpisode": "એપિસોડ કાઢી નાખો", + "HeaderRemoveEpisodes": "{0} એપિસોડ્સ કાઢી નાખો", + "HeaderRSSFeedGeneral": "સામાન્ય RSS ફીડ", + "HeaderRSSFeedIsOpen": "RSS ફીડ ખોલેલી છે", + "HeaderRSSFeeds": "RSS ફીડ્સ", + "HeaderSavedMediaProgress": "સાચવેલ મીડિયા પ્રગતિ", + "HeaderSchedule": "સમયપત્રક", + "HeaderScheduleLibraryScans": "પુસ્તકાલય સ્કેન સમયપત્રક", + "HeaderSession": "સેશન", + "HeaderSetBackupSchedule": "બેકઅપ સમયપત્રક સેટ કરો", "HeaderStatsLongestItems": "Longest Items (hrs)", "HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)", "HeaderStatsRecentSessions": "Recent Sessions", From 23fa9e8d7f88495bd2c16f7b303649bc71a45dd3 Mon Sep 17 00:00:00 2001 From: Dr-Blank <64108942+Dr-Blank@users.noreply.github.com> Date: Mon, 6 Nov 2023 18:15:18 -0500 Subject: [PATCH 131/208] Update gu.json --- client/strings/gu.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/strings/gu.json b/client/strings/gu.json index 9a1057224..3504916e5 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -149,9 +149,15 @@ "HeaderScheduleLibraryScans": "પુસ્તકાલય સ્કેન સમયપત્રક", "HeaderSession": "સેશન", "HeaderSetBackupSchedule": "બેકઅપ સમયપત્રક સેટ કરો", - "HeaderStatsLongestItems": "Longest Items (hrs)", - "HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)", - "HeaderStatsRecentSessions": "Recent Sessions", + "HeaderSettings": "સેટિંગ્સ", + "HeaderSettingsDisplay": "ડિસ્પ્લે સેટિંગ્સ", + "HeaderSettingsExperimental": "પ્રયોગશીલ સેટિંગ્સ", + "HeaderSettingsGeneral": "સામાન્ય સેટિંગ્સ", + "HeaderSettingsScanner": "સ્કેનર સેટિંગ્સ", + "HeaderSleepTimer": "સ્લીપ ટાઈમર", + "HeaderStatsLargestItems": "સૌથી મોટી વસ્તુઓ", + "HeaderStatsLongestItems": "સૌથી લાંબી વસ્તુઓ (કલાક)", + "HeaderStatsMinutesListeningChart": "સાંભળવાની મિનિટ (છેલ્લા ૭ દિવસ)", "HeaderStatsTop10Authors": "Top 10 Authors", "HeaderStatsTop5Genres": "Top 5 Genres", "HeaderTableOfContents": "Table of Contents", From 6d968f90448ba99b39629b12295a53267b229780 Mon Sep 17 00:00:00 2001 From: Dr-Blank <64108942+Dr-Blank@users.noreply.github.com> Date: Mon, 6 Nov 2023 18:16:03 -0500 Subject: [PATCH 132/208] Update gu.json --- client/strings/gu.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/strings/gu.json b/client/strings/gu.json index 3504916e5..ae57c7a2b 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -158,6 +158,7 @@ "HeaderStatsLargestItems": "સૌથી મોટી વસ્તુઓ", "HeaderStatsLongestItems": "સૌથી લાંબી વસ્તુઓ (કલાક)", "HeaderStatsMinutesListeningChart": "સાંભળવાની મિનિટ (છેલ્લા ૭ દિવસ)", + "HeaderStatsRecentSessions": "છેલ્લી સાંભળતી સેશન્સ", "HeaderStatsTop10Authors": "Top 10 Authors", "HeaderStatsTop5Genres": "Top 5 Genres", "HeaderTableOfContents": "Table of Contents", From 819c524f5192306d2aec5dd1ee2e9a929755714a Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 8 Nov 2023 16:19:24 +0000 Subject: [PATCH 133/208] Pass audnexus to AuthorCandidates constructor directly --- server/finders/BookFinder.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index fa034bce0..ac3de2a75 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -233,15 +233,15 @@ class BookFinder { } static AuthorCandidates = class { - constructor(bookFinder, cleanAuthor) { - this.bookFinder = bookFinder + constructor(cleanAuthor, audnexus) { + this.audnexus = audnexus this.candidates = new Set() this.cleanAuthor = cleanAuthor if (cleanAuthor) this.candidates.add(cleanAuthor) } validateAuthor(name, region = '', maxLevenshtein = 2) { - return this.bookFinder.audnexus.authorASINsRequest(name, region).then((asins) => { + return this.audnexus.authorASINsRequest(name, region).then((asins) => { for (const [i, asin] of asins.entries()) { if (i > 10) break let cleanName = cleanAuthorForCompares(asin.name) @@ -326,7 +326,7 @@ class BookFinder { const cleanAuthor = cleanAuthorForCompares(author) // Now run up to maxFuzzySearches fuzzy searches - let authorCandidates = new BookFinder.AuthorCandidates(this, cleanAuthor) + let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus) // Remove underscores and parentheses with their contents, and replace with a separator const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, " - ") From 49e4515785c177058a4da9427130e9a6adebb44f Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 8 Nov 2023 16:21:20 +0000 Subject: [PATCH 134/208] Add stripRedudantSpaces --- server/finders/BookFinder.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index ac3de2a75..7d26b6bfc 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -439,6 +439,8 @@ function replaceAccentedChars(str) { function cleanTitleForCompares(title) { if (!title) return '' + title = stripRedundantSpaces(title) + // Remove subtitle if there (i.e. "Cool Book: Coolest Ever" becomes "Cool Book") let stripped = stripSubtitle(title) @@ -452,6 +454,8 @@ function cleanTitleForCompares(title) { function cleanAuthorForCompares(author) { if (!author) return '' + author = stripRedundantSpaces(author) + let cleanAuthor = replaceAccentedChars(author).toLowerCase() // separate initials cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2') @@ -459,3 +463,7 @@ function cleanAuthorForCompares(author) { cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '') return cleanAuthor } + +function stripRedundantSpaces(str) { + return str.replace(/\s+/g, ' ').trim() +} From 2730486ba58384b6fb1696d2b1ffa803889c1ade Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 8 Nov 2023 16:24:08 +0000 Subject: [PATCH 135/208] Add tests for AuthorCandidates and search() in BookFinder --- server/finders/bookFinder.test.js | 355 ++++++++++++++++++++++++++---- 1 file changed, 308 insertions(+), 47 deletions(-) diff --git a/server/finders/bookFinder.test.js b/server/finders/bookFinder.test.js index ab82b0f9b..c204b99a9 100644 --- a/server/finders/bookFinder.test.js +++ b/server/finders/bookFinder.test.js @@ -1,62 +1,323 @@ const bookFinder = require('./BookFinder') +const Audnexus = require('../providers/Audnexus') +const { LogLevel } = require('../utils/constants') +const Logger = require('../Logger') +jest.mock('../providers/Audnexus') -describe('TitleCandidates with author', () => { - let titleCandidates +Logger.setLogLevel(LogLevel.INFO) - beforeEach(() => { - titleCandidates = new bookFinder.constructor.TitleCandidates('leo tolstoy') - }) +describe('TitleCandidates', () => { + describe('cleanAuthor non-empty', () => { + let titleCandidates + let cleanAuthor = 'leo tolstoy' - describe('single add', () => { - it.each([ - ['adds a clean title to candidates', 'anna karenina', ['anna karenina']], - ['lowercases candidate title', 'ANNA KARENINA', ['anna karenina']], - ['removes author name from title', 'anna karenina by leo tolstoy', ['anna karenina']], - ['removes author name title', 'leo tolstoy', []], - ['cleans subtitle from title', 'anna karenina: subtitle', ['anna karenina']], - ['removes "by ..." from title', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], - ['removes bitrate from title', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], - ['removes edition from title 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], - ['removes edition from title 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], - ['removes file-type from title', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']], - ['removes "a novel" from title', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']], - ['removes preceding/trailing numbers from title', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']], - ['does not add empty title', '', []], - ['does not add title with only spaces', ' ', []], - ['adds digit-only title, but not its empty string transformation', '1984', ['1984']], - ])('%s', (_, title, expected) => { - titleCandidates.add(title) - expect(titleCandidates.getCandidates()).toEqual(expected) + beforeEach(() => { + titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) + }) + + describe('single add', () => { + it.each([ + ['adds a clean title to candidates', 'anna karenina', ['anna karenina']], + ['lowercases candidate title', 'ANNA KARENINA', ['anna karenina']], + ['removes author name from title', `anna karenina by ${cleanAuthor}`, ['anna karenina']], + ['removes author name title', cleanAuthor, []], + ['cleans subtitle from title', 'anna karenina: subtitle', ['anna karenina']], + ['removes "by ..." from title', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], + ['removes bitrate from title', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], + ['removes edition from title 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], + ['removes edition from title 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], + ['removes file-type from title', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']], + ['removes "a novel" from title', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']], + ['removes preceding/trailing numbers from title', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']], + ['does not add empty title', '', []], + ['does not add title with only spaces', ' ', []], + ['adds digit-only title, but not its empty string transformation', '1984', ['1984']], + ])('%s', (_, title, expected) => { + titleCandidates.add(title) + expect(titleCandidates.getCandidates()).toEqual(expected) + }) + }) + + describe('multi add', () => { + it.each([ + ['digits-only candidates get lower priority', ['01', 'anna karenina'], ['anna karenina', '01']], + ['transformed candidates get higher priority', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']], + ['other candidates are ordered by position', ['title1', 'title2'], ['title1', 'title2']], + ['author candidate is removed', ['title1', cleanAuthor], ['title1']], + ])('%s', (_, titles, expected) => { + for (const title of titles) titleCandidates.add(title) + expect(titleCandidates.getCandidates()).toEqual(expected) + }) }) }) - describe('multi add', () => { - it.each([ - ['digits-only candidates get lower priority', ['01', 'anna karenina'], ['anna karenina', '01']], - ['transformed candidates get higher priority', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']], - ['other candidates are ordered by position', ['title1', 'title2'], ['title1', 'title2']], - ['author candidate is removed', ['title1', 'leo tolstoy'], ['title1']], - ])('%s', (_, titles, expected) => { - for (const title of titles) titleCandidates.add(title) - expect(titleCandidates.getCandidates()).toEqual(expected) + describe('cleanAuthor empty', () => { + let titleCandidates + let cleanAuthor = '' + + beforeEach(() => { + titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) + }) + + describe('single add', () => { + it.each([ + ['does not remove author name', 'leo tolstoy', ['leo tolstoy']], + ])('%s', (_, title, expected) => { + titleCandidates.add(title) + expect(titleCandidates.getCandidates()).toEqual(expected) + }) + }) + }) +}) + + +describe('AuthorCandidates', () => { + let authorCandidates + const audnexus = new Audnexus() + audnexus.authorASINsRequest.mockResolvedValue([ + { name: 'Leo Tolstoy' }, + { name: 'Nikolai Gogol' }, + { name: 'J. K. Rowling' }, + ]) + + describe('cleanAuthor is null', () => { + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(null, audnexus) + }) + + describe('no add', () => { + it.each([ + ['returns empty author', []], + ])('%s', async (_, expected) => { + expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) + }) + }) + + describe('single add', () => { + it.each([ + ['returns valid author', 'nikolai gogol', ['nikolai gogol']], + ['does not return invalid author (not in list)', 'fyodor dostoevsky', []], + ['returns valid author (valid is a substring of added)', 'dr. nikolai gogol', ['nikolai gogol']], + ['returns added author (added is a substring of valid)', 'gogol', ['gogol']], + ['returns valid author (added is similar to valid)', 'nicolai gogol', ['nikolai gogol']], + ['does not return invalid author (added too distant)', 'nikolai google', []], + ['returns valid author (contains redundant spaces)', 'nikolai gogol', ['nikolai gogol']], + ['returns valid author (normalized initials)', 'j.k. rowling', ['j. k. rowling']], + ])('%s', async (_, author, expected) => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) + }) + }) + + describe('multi add', () => { + it.each([ + ['returns valid authors', ['nikolai gogol', 'leo tolstoy'], ['nikolai gogol', 'leo tolstoy']], + ['returns deduped valid authors', ['nikolai gogol', 'nikolai gogol'], ['nikolai gogol']], + ])('%s', async (_, authors, expected) => { + for (const author of authors) authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) + }) + }) + }) + + describe('cleanAuthor is valid', () => { + const cleanAuthor = 'leo tolstoy' + + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + }) + + describe('no add', () => { + it.each([ + ['returns clean author from constructor', [cleanAuthor]], + ])('%s', async (_, expected) => { + expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) + }) + }) + + describe('single add', () => { + it.each([ + ['returns cleanAuthor + valid author', 'nikolai gogol', [cleanAuthor, 'nikolai gogol']], + ['returns deduplicated author', cleanAuthor, [cleanAuthor]], + ])('%s', async (_, author, expected) => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) + }) + }) + }) + + + describe('cleanAuthor is invalid', () => { + const cleanAuthor = 'fyodor dostoevsky' + + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + }) + + describe('no add', () => { + it.each([ + ['returns invalid clean author from constructor', [cleanAuthor]], + ])('%s', async (_, expected) => { + expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) + }) + }) + + describe('single add', () => { + it.each([ + ['returns only valid author', 'nikolai gogol', ['nikolai gogol']], + ])('%s', async (_, author, expected) => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) + }) + }) + }) + + describe('cleanAuthor is invalid and dirty', () => { + describe('no add', () => { + it.each([ + ['returns invalid aggressively cleanAuthor from constructor', 'fyodor dostoevsky, translated by jackie chan', ['fyodor dostoevsky']], + ['returns invalid cleanAuthor from constructor (empty after aggressive ckean)', ', jackie chan', [', jackie chan']], + ])('%s', async (_, cleanAuthor, expected) => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) + }) }) }) }) -describe('TitleCandidates with no author', () => { - let titleCandidates +describe('search', () => { + const t = 'title' + const a = 'author' + const u = 'unknown' + const r = ['book'] - beforeEach(() => { - titleCandidates = new bookFinder.constructor.TitleCandidates('') - }) - - describe('single add', () => { - it.each([ - ['does not removes author name', 'leo tolstoy', ['leo tolstoy']], - ])('%s', (_, title, expected) => { - titleCandidates.add(title) - expect(titleCandidates.getCandidates()).toEqual(expected) + bookFinder.runSearch = jest.fn((searchTitle, searchAuthor) => { + return new Promise((resolve) => { + resolve(searchTitle == t && (searchAuthor == a || searchAuthor == u) ? r : []) }) }) -}) + + const audnexus = new Audnexus() + audnexus.authorASINsRequest.mockResolvedValue([ + { name: a }, + ]) + bookFinder.audnexus = audnexus + beforeEach(() => { + bookFinder.runSearch.mockClear() + }) + + describe('no or empty title', () => { + it('returns empty result', async () => { + expect(await bookFinder.search('', '', a)).toEqual([]) + expect(bookFinder.runSearch).toHaveBeenCalledTimes(0) + }) + }) + + describe('exact valid title and exact valid author', () => { + it('returns result (no fuzzy searches)', async () => { + expect(await bookFinder.search('', t, a)).toEqual(r) + expect(bookFinder.runSearch).toHaveBeenCalledTimes(1) + }) + }) + + describe('contains valid title and exact valid author', () => { + it.each([ + [`${t} -`], + [`${t} - ${a}`], + [`${a} - ${t}`], + [`${t}- ${a}`], + [`${t} -${a}`], + [`${t} ${a}`], + [`${a} - ${t} (unabridged)`], + [`${a} - ${t} (subtitle) - mp3`], + [`${t} {narrator} - series-01 64kbps 10:00:00`], + [`${a} - ${t} (2006) narrated by narrator [unabridged]`], + [`${t} - ${a} 2022 mp3`], + [`01 ${t}`], + [`2022_${t}_HQ`], +// [`${a} - ${t}`], + ])(`returns result ('%s', '${a}') (1 fuzzy search)` , async (searchTitle) => { + expect(await bookFinder.search('', searchTitle, a)).toEqual(r) + expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) + }) + + + it.each([ + [`s-01 - ${t} (narrator) 64kbps 10:00:00`], + [`${a} - series 01 - ${t}`], +// [`${a} - ${t}`], + ])(`returns result ('%s', '${a}') (2 fuzzy searches)` , async (searchTitle) => { + expect(await bookFinder.search('', searchTitle, a)).toEqual(r) + expect(bookFinder.runSearch).toHaveBeenCalledTimes(3) + }) + + it.each([ + [`${t}-${a}`], + [`${t} junk`], + ])(`returns empty result ('%s', '${a}')`, async (searchTitle) => { + expect(await bookFinder.search('', searchTitle, a)).toEqual([]) + }) + + describe('maxFuzzySearches = 0', () => { + it.each([ + [`${t} - ${a}`], + ])(`returns empty result ('%s', '${a}') (no fuzzy search)` , async (searchTitle) => { + expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 0 })).toEqual([]) + expect(bookFinder.runSearch).toHaveBeenCalledTimes(1) + }) + }) + + describe('maxFuzzySearches = 1', () => { + it.each([ + [`s-01 - ${t} (narrator) 64kbps 10:00:00`], + [`${a} - series 01 - ${t}`], + ])(`returns empty result ('%s', '${a}') (1 fuzzy search)` , async (searchTitle) => { + expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 1 })).toEqual([]) + expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) + }) + }) + }) + + describe('contains valid title and no author', () => { + it.each([ + [`${t} - ${a}`], + [`${a} - ${t}`], + ])(`returns result ('%s', '') (1 fuzzy search)` , async (searchTitle) => { + expect(await bookFinder.search('', searchTitle, '')).toEqual(r) + expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) + }) + + it.each([ + [`${t}`], + [`${t} - ${u}`], + [`${u} - ${t}`], + ])(`returns empty result ('%s', '') (no fuzzy search)` , async (searchTitle) => { + expect(await bookFinder.search('', searchTitle, '')).toEqual([]) + }) + }) + + describe('contains valid title and unknown author', () => { + it.each([ + [`${t} - ${u}`], + [`${u} - ${t}`], + ])(`returns result ('%s', '') (1 fuzzy search)` , async (searchTitle) => { + expect(await bookFinder.search('', searchTitle, u)).toEqual(r) + expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) + }) +/* + it.each([ + ])(`returns result ('%s', '') (2 fuzzy searches)` , async (searchTitle) => { + expect(await bookFinder.search('', searchTitle, u)).toEqual(r) + expect(bookFinder.runSearch).toHaveBeenCalledTimes(3) + }) +*/ + it.each([ + [`${t}`], + ])(`returns result ('%s', '') (no fuzzy search)` , async (searchTitle) => { + expect(await bookFinder.search('', searchTitle, u)).toEqual(r) + expect(bookFinder.runSearch).toHaveBeenCalledTimes(1) + }) + }) + +}) \ No newline at end of file From d1671f0ddc0e8c557de0773bf685cab7a380949c Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 8 Nov 2023 16:37:12 +0000 Subject: [PATCH 136/208] Cleanup commented out tests --- server/finders/bookFinder.test.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/server/finders/bookFinder.test.js b/server/finders/bookFinder.test.js index c204b99a9..2c54c8804 100644 --- a/server/finders/bookFinder.test.js +++ b/server/finders/bookFinder.test.js @@ -236,7 +236,6 @@ describe('search', () => { [`${t} - ${a} 2022 mp3`], [`01 ${t}`], [`2022_${t}_HQ`], -// [`${a} - ${t}`], ])(`returns result ('%s', '${a}') (1 fuzzy search)` , async (searchTitle) => { expect(await bookFinder.search('', searchTitle, a)).toEqual(r) expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) @@ -246,7 +245,6 @@ describe('search', () => { it.each([ [`s-01 - ${t} (narrator) 64kbps 10:00:00`], [`${a} - series 01 - ${t}`], -// [`${a} - ${t}`], ])(`returns result ('%s', '${a}') (2 fuzzy searches)` , async (searchTitle) => { expect(await bookFinder.search('', searchTitle, a)).toEqual(r) expect(bookFinder.runSearch).toHaveBeenCalledTimes(3) @@ -305,13 +303,7 @@ describe('search', () => { expect(await bookFinder.search('', searchTitle, u)).toEqual(r) expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) }) -/* - it.each([ - ])(`returns result ('%s', '') (2 fuzzy searches)` , async (searchTitle) => { - expect(await bookFinder.search('', searchTitle, u)).toEqual(r) - expect(bookFinder.runSearch).toHaveBeenCalledTimes(3) - }) -*/ + it.each([ [`${t}`], ])(`returns result ('%s', '') (no fuzzy search)` , async (searchTitle) => { From e140897313b4cb7cbdc137f38a3c1b8901aadaf1 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 8 Nov 2023 14:45:29 -0600 Subject: [PATCH 137/208] Add match existing user by and auto register settings and UI --- client/pages/config/authentication.vue | 100 ++++++++++++++-------- server/objects/settings/ServerSettings.js | 12 ++- 2 files changed, 74 insertions(+), 38 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 7cedfd25a..0da486c11 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -12,45 +12,57 @@ <ui-checkbox v-model="enableOpenIDAuth" checkbox-bg="bg" /> <p class="text-lg pl-4">OpenID Connect Authentication</p> </div> - <div class="overflow-hidden"> - <transition name="slide"> - <div v-if="enableOpenIDAuth" class="flex flex-wrap pt-4"> - <div class="w-full flex items-center mb-2"> - <div class="flex-grow"> - <ui-text-input-with-label ref="issuerUrl" v-model="newAuthSettings.authOpenIDIssuerURL" :disabled="savingSettings" :label="'Issuer URL'" /> - </div> - <div class="w-36 mx-1 mt-[1.375rem]"> - <ui-btn class="h-[2.375rem] text-sm inline-flex items-center justify-center w-full" type="button" :padding-y="0" :padding-x="4" @click.stop="autoPopulateOIDCClick"> - <span class="material-icons text-base">auto_fix_high</span> - <span class="whitespace-nowrap break-keep pl-1">Auto-populate</span></ui-btn - > - </div> + + <transition name="slide"> + <div v-if="enableOpenIDAuth" class="flex flex-wrap pt-4"> + <div class="w-full flex items-center mb-2"> + <div class="flex-grow"> + <ui-text-input-with-label ref="issuerUrl" v-model="newAuthSettings.authOpenIDIssuerURL" :disabled="savingSettings" :label="'Issuer URL'" /> </div> - - <ui-text-input-with-label ref="authorizationUrl" v-model="newAuthSettings.authOpenIDAuthorizationURL" :disabled="savingSettings" :label="'Authorize URL'" class="mb-2" /> - - <ui-text-input-with-label ref="tokenUrl" v-model="newAuthSettings.authOpenIDTokenURL" :disabled="savingSettings" :label="'Token URL'" class="mb-2" /> - - <ui-text-input-with-label ref="userInfoUrl" v-model="newAuthSettings.authOpenIDUserInfoURL" :disabled="savingSettings" :label="'Userinfo URL'" class="mb-2" /> - - <ui-text-input-with-label ref="jwksUrl" v-model="newAuthSettings.authOpenIDJwksURL" :disabled="savingSettings" :label="'JWKS URL'" class="mb-2" /> - - <ui-text-input-with-label ref="logoutUrl" v-model="newAuthSettings.authOpenIDLogoutURL" :disabled="savingSettings" :label="'Logout URL'" class="mb-2" /> - - <ui-text-input-with-label ref="openidClientId" v-model="newAuthSettings.authOpenIDClientID" :disabled="savingSettings" :label="'Client ID'" class="mb-2" /> - - <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" /> - - <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="'Button Text'" class="mb-2" /> - - <div class="flex items-center py-2 px-1"> - <ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" /> - <p id="auto-redirect-toggle" class="pl-4">Auto Launch</p> - <p class="pl-4 text-sm text-gray-300">Redirect to the auth provider automatically when navigating to the /login page</p> + <div class="w-36 mx-1 mt-[1.375rem]"> + <ui-btn class="h-[2.375rem] text-sm inline-flex items-center justify-center w-full" type="button" :padding-y="0" :padding-x="4" @click.stop="autoPopulateOIDCClick"> + <span class="material-icons text-base">auto_fix_high</span> + <span class="whitespace-nowrap break-keep pl-1">Auto-populate</span></ui-btn + > </div> </div> - </transition> - </div> + + <ui-text-input-with-label ref="authorizationUrl" v-model="newAuthSettings.authOpenIDAuthorizationURL" :disabled="savingSettings" :label="'Authorize URL'" class="mb-2" /> + + <ui-text-input-with-label ref="tokenUrl" v-model="newAuthSettings.authOpenIDTokenURL" :disabled="savingSettings" :label="'Token URL'" class="mb-2" /> + + <ui-text-input-with-label ref="userInfoUrl" v-model="newAuthSettings.authOpenIDUserInfoURL" :disabled="savingSettings" :label="'Userinfo URL'" class="mb-2" /> + + <ui-text-input-with-label ref="jwksUrl" v-model="newAuthSettings.authOpenIDJwksURL" :disabled="savingSettings" :label="'JWKS URL'" class="mb-2" /> + + <ui-text-input-with-label ref="logoutUrl" v-model="newAuthSettings.authOpenIDLogoutURL" :disabled="savingSettings" :label="'Logout URL'" class="mb-2" /> + + <ui-text-input-with-label ref="openidClientId" v-model="newAuthSettings.authOpenIDClientID" :disabled="savingSettings" :label="'Client ID'" class="mb-2" /> + + <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" /> + + <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="'Button Text'" class="mb-2" /> + + <div class="flex items-center pt-1 mb-2"> + <div class="w-44"> + <ui-dropdown v-model="newAuthSettings.authOpenIDMatchExistingBy" small :items="matchingExistingOptions" label="Match existing users by" :disabled="savingSettings" /> + </div> + <p class="pl-4 text-sm text-gray-300 mt-5">Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider</p> + </div> + + <div class="flex items-center py-4 px-1"> + <ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" /> + <p id="auto-redirect-toggle" class="pl-4">Auto Launch</p> + <p class="pl-4 text-sm text-gray-300">Redirect to the auth provider automatically when navigating to the login page</p> + </div> + + <div class="flex items-center py-4 px-1"> + <ui-toggle-switch labeledBy="auto-register-toggle" v-model="newAuthSettings.authOpenIDAutoRegister" :disabled="savingSettings" /> + <p id="auto-register-toggle" class="pl-4">Auto Register</p> + <p class="pl-4 text-sm text-gray-300">Automatically create new users after logging in</p> + </div> + </div> + </transition> </div> <div class="w-full flex items-center justify-end p-4"> <ui-btn color="success" :padding-x="8" small class="text-base" :loading="savingSettings" @click="saveSettings">{{ $strings.ButtonSave }}</ui-btn> @@ -90,6 +102,22 @@ export default { computed: { authMethods() { return this.authSettings.authActiveAuthMethods || [] + }, + matchingExistingOptions() { + return [ + { + text: 'Do not match', + value: null + }, + { + text: 'Match by email', + value: 'email' + }, + { + text: 'Match by username', + value: 'username' + } + ] } }, methods: { diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 781943b49..05a64d065 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -74,6 +74,8 @@ class ServerSettings { this.authOpenIDClientSecret = '' this.authOpenIDButtonText = 'Login with OpenId' this.authOpenIDAutoLaunch = false + this.authOpenIDAutoRegister = false + this.authOpenIDMatchExistingBy = null if (settings) { this.construct(settings) @@ -130,6 +132,8 @@ class ServerSettings { this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' this.authOpenIDButtonText = settings.authOpenIDButtonText || 'Login with OpenId' this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch + this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister + this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] @@ -234,7 +238,9 @@ class ServerSettings { authOpenIDClientID: this.authOpenIDClientID, // Do not return to client authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client authOpenIDButtonText: this.authOpenIDButtonText, - authOpenIDAutoLaunch: this.authOpenIDAutoLaunch + authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, + authOpenIDAutoRegister: this.authOpenIDAutoRegister, + authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy } } @@ -263,7 +269,9 @@ class ServerSettings { authOpenIDClientID: this.authOpenIDClientID, // Do not return to client authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client authOpenIDButtonText: this.authOpenIDButtonText, - authOpenIDAutoLaunch: this.authOpenIDAutoLaunch + authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, + authOpenIDAutoRegister: this.authOpenIDAutoRegister, + authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy } } From ee75d672e6d46c324f817e949aea72cd487d04b4 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 8 Nov 2023 16:14:57 -0600 Subject: [PATCH 138/208] Matching user by openid sub, email or username based on server settings. Auto register user. Persist sub on User records --- server/Auth.js | 53 ++++++++++++++++--- server/models/User.js | 102 +++++++++++++++++++++++++++++++----- server/objects/user/User.js | 9 +++- 3 files changed, 142 insertions(+), 22 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 361380f8f..eeb7ad472 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -82,14 +82,51 @@ class Auth { scope: 'openid profile email' } }, async (tokenset, userinfo, done) => { - // TODO: Here is where to lookup the Abs user or register a new Abs user Logger.debug(`[Auth] openid callback userinfo=`, userinfo) - let user = null - // TODO: Temporary lookup existing user by email. May be replaced by a setting to toggle this or use name - if (userinfo.email && userinfo.email_verified) { - user = await Database.userModel.getUserByEmail(userinfo.email) - // TODO: If using existing user then save userinfo.sub on user + if (!userinfo.sub) { + Logger.error(`[Auth] openid callback invalid userinfo, no sub`) + return done(null, null) + } + + // First check for matching user by sub + let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) + if (!user) { + // Optionally match existing by email or username based on server setting "authOpenIDMatchExistingBy" + if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) { + Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`) + user = await Database.userModel.getUserByEmail(userinfo.email) + // Check that user is not already matched + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) + // TODO: Show some error log? + user = null + } + } else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) { + Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`) + user = await Database.userModel.getUserByUsername(userinfo.preferred_username) + // Check that user is not already matched + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) + // TODO: Show some error log? + user = null + } + } + + // If existing user was matched and isActive then save sub to user + if (user?.isActive) { + Logger.info(`[Auth] openid: New user found matching existing user "${user.username}"`) + user.authOpenIDSub = userinfo.sub + await Database.userModel.updateFromOld(user) + } else if (user && !user.isActive) { + Logger.warn(`[Auth] openid: New user found matching existing user "${user.username}" but that user is deactivated`) + } + + // Optionally auto register the user + if (!user && Database.serverSettings.authOpenIDAutoRegister) { + Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo) + user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this) + } } if (!user?.isActive) { @@ -368,7 +405,7 @@ class Auth { /** * Function to generate a jwt token for a given user * - * @param {Object} user + * @param {{ id:string, username:string }} user * @returns {string} token */ generateAccessToken(user) { @@ -405,7 +442,7 @@ class Auth { const users = await Database.userModel.getOldUsers() if (users.length) { for (const user of users) { - user.token = await this.generateAccessToken({ userId: user.id, username: user.username }) + user.token = await this.generateAccessToken(user) } await Database.updateBulkUsers(users) } diff --git a/server/models/User.js b/server/models/User.js index d3028d9a0..4c348f42f 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -1,7 +1,9 @@ const uuidv4 = require("uuid").v4 -const { DataTypes, Model, Op } = require('sequelize') +const sequelize = require('sequelize') const Logger = require('../Logger') const oldUser = require('../objects/user/User') +const SocketAuthority = require('../SocketAuthority') +const { DataTypes, Model } = sequelize class User extends Model { constructor(values, options) { @@ -46,6 +48,12 @@ class User extends Model { return users.map(u => this.getOldUser(u)) } + /** + * Get old user model from new + * + * @param {Object} userExpanded + * @returns {oldUser} + */ static getOldUser(userExpanded) { const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress()) @@ -72,15 +80,27 @@ class User extends Model { createdAt: userExpanded.createdAt.valueOf(), permissions, librariesAccessible, - itemTagsSelected + itemTagsSelected, + authOpenIDSub: userExpanded.extraData?.authOpenIDSub || null }) } + /** + * + * @param {oldUser} oldUser + * @returns {Promise<User>} + */ static createFromOld(oldUser) { const user = this.getFromOld(oldUser) return this.create(user) } + /** + * Update User from old user model + * + * @param {oldUser} oldUser + * @returns {Promise<boolean>} + */ static updateFromOld(oldUser) { const user = this.getFromOld(oldUser) return this.update(user, { @@ -93,7 +113,21 @@ class User extends Model { }) } + /** + * Get new User model from old + * + * @param {oldUser} oldUser + * @returns {Object} + */ static getFromOld(oldUser) { + const extraData = { + seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], + oldUserId: oldUser.oldUserId + } + if (oldUser.authOpenIDSub) { + extraData.authOpenIDSub = oldUser.authOpenIDSub + } + return { id: oldUser.id, username: oldUser.username, @@ -103,10 +137,7 @@ class User extends Model { token: oldUser.token || null, isActive: !!oldUser.isActive, lastSeen: oldUser.lastSeen || null, - extraData: { - seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], - oldUserId: oldUser.oldUserId - }, + extraData, createdAt: oldUser.createdAt || Date.now(), permissions: { ...oldUser.permissions, @@ -130,12 +161,12 @@ class User extends Model { * @param {string} username * @param {string} pash * @param {Auth} auth - * @returns {oldUser} + * @returns {Promise<oldUser>} */ static async createRootUser(username, pash, auth) { const userId = uuidv4() - const token = await auth.generateAccessToken({ userId, username }) + const token = await auth.generateAccessToken({ id: userId, username }) const newRoot = new oldUser({ id: userId, @@ -150,6 +181,38 @@ class User extends Model { return newRoot } + /** + * Create user from openid userinfo + * @param {Object} userinfo + * @param {Auth} auth + * @returns {Promise<oldUser>} + */ + static async createUserFromOpenIdUserInfo(userinfo, auth) { + const userId = uuidv4() + // TODO: Ensure username is unique? + const username = userinfo.preferred_username || userinfo.name || userinfo.sub + const email = (userinfo.email && userinfo.email_verified) ? userinfo.email : null + + const token = await auth.generateAccessToken({ id: userId, username }) + + const newUser = new oldUser({ + id: userId, + type: 'user', + username, + email, + pash: null, + token, + isActive: true, + authOpenIDSub: userinfo.sub, + createdAt: Date.now() + }) + if (await this.createFromOld(newUser)) { + SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser()) + return newUser + } + return null + } + /** * Get a user by id or by the old database id * @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id @@ -160,13 +223,13 @@ class User extends Model { if (!userId) return null const user = await this.findOne({ where: { - [Op.or]: [ + [sequelize.Op.or]: [ { id: userId }, { extraData: { - [Op.substring]: userId + [sequelize.Op.substring]: userId } } ] @@ -187,7 +250,7 @@ class User extends Model { const user = await this.findOne({ where: { username: { - [Op.like]: username + [sequelize.Op.like]: username } }, include: this.sequelize.models.mediaProgress @@ -206,7 +269,7 @@ class User extends Model { const user = await this.findOne({ where: { email: { - [Op.like]: email + [sequelize.Op.like]: email } }, include: this.sequelize.models.mediaProgress @@ -229,6 +292,21 @@ class User extends Model { return this.getOldUser(user) } + /** + * Get user by openid sub + * @param {string} sub + * @returns {Promise<oldUser|null>} returns null if not found + */ + static async getUserByOpenIDSub(sub) { + if (!sub) return null + const user = await this.findOne({ + where: sequelize.where(sequelize.literal(`extraData->>"authOpenIDSub"`), sub), + include: this.sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + /** * Get array of user id and username * @returns {object[]} { id, username } diff --git a/server/objects/user/User.js b/server/objects/user/User.js index 5192752a3..b503872d6 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -24,6 +24,8 @@ class User { this.librariesAccessible = [] // Library IDs (Empty if ALL libraries) this.itemTagsSelected = [] // Empty if ALL item tags accessible + this.authOpenIDSub = null + if (user) { this.construct(user) } @@ -66,7 +68,7 @@ class User { getDefaultUserPermissions() { return { download: true, - update: true, + update: this.type === 'root' || this.type === 'admin', delete: this.type === 'root', upload: this.type === 'root' || this.type === 'admin', accessAllLibraries: true, @@ -93,7 +95,8 @@ class User { createdAt: this.createdAt, permissions: this.permissions, librariesAccessible: [...this.librariesAccessible], - itemTagsSelected: [...this.itemTagsSelected] + itemTagsSelected: [...this.itemTagsSelected], + authOpenIDSub: this.authOpenIDSub } } @@ -186,6 +189,8 @@ class User { this.librariesAccessible = [...(user.librariesAccessible || [])] this.itemTagsSelected = [...(user.itemTagsSelected || [])] + + this.authOpenIDSub = user.authOpenIDSub || null } update(payload) { From 8f4c75ff2b9f8bad28ce73ac671fded891e9a0a2 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 8 Nov 2023 16:28:05 -0600 Subject: [PATCH 139/208] Update:Author card books translation string #2284 --- client/components/cards/AuthorCard.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue index db4e7e9a7..fc3bc4b26 100644 --- a/client/components/cards/AuthorCard.vue +++ b/client/components/cards/AuthorCard.vue @@ -8,7 +8,7 @@ <!-- Author name & num books overlay --> <div v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2"> <p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> - <p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p> + <p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} {{ $strings.LabelBooks }}</p> </div> <!-- Search icon btn --> From bf48eee705101454d80c80c566e85869e83930c3 Mon Sep 17 00:00:00 2001 From: burghy86 <burghy@mail.com> Date: Thu, 9 Nov 2023 15:46:25 +0100 Subject: [PATCH 140/208] Update it.json arrange the additional lines. how the hell did we get to over 700 lines in less than two months? --- client/strings/it.json | 80 +++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/client/strings/it.json b/client/strings/it.json index 747d7420c..c893212e1 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -1,10 +1,10 @@ { "ButtonAdd": "Aggiungi", "ButtonAddChapters": "Aggiungi Capitoli", - "ButtonAddDevice": "Add Device", - "ButtonAddLibrary": "Add Library", + "ButtonAddDevice": "Aggiungi Dispositivo", + "ButtonAddLibrary": "Aggiungi Libreria", "ButtonAddPodcasts": "Aggiungi Podcast", - "ButtonAddUser": "Add User", + "ButtonAddUser": "Aggiungi User", "ButtonAddYourFirstLibrary": "Aggiungi la tua prima libreria", "ButtonApply": "Applica", "ButtonApplyChapters": "Applica", @@ -62,7 +62,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla", "ButtonReScan": "Ri-scansiona", "ButtonReset": "Reset", - "ButtonResetToDefault": "Reset to default", + "ButtonResetToDefault": "Ripristino di default", "ButtonRestore": "Ripristina", "ButtonSave": "Salva", "ButtonSaveAndClose": "Salva & Chiudi", @@ -75,7 +75,7 @@ "ButtonSetChaptersFromTracks": "Impostare i capitoli dalle tracce", "ButtonShiftTimes": "Ricerca veloce", "ButtonShow": "Mostra", - "ButtonStartM4BEncode": "Inizia L'Encoda del M4B", + "ButtonStartM4BEncode": "Inizia L'Encode del M4B", "ButtonStartMetadataEmbed": "Inizia Incorporo Metadata", "ButtonSubmit": "Invia", "ButtonTest": "Test", @@ -102,7 +102,7 @@ "HeaderCurrentDownloads": "Download Correnti", "HeaderDetails": "Dettagli", "HeaderDownloadQueue": "Download Queue", - "HeaderEbookFiles": "Ebook Files", + "HeaderEbookFiles": "Ebook File", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "Episodi", @@ -161,7 +161,7 @@ "HeaderStatsRecentSessions": "Sessioni Recenti", "HeaderStatsTop10Authors": "Top 10 Autori", "HeaderStatsTop5Genres": "Top 5 Generi", - "HeaderTableOfContents": "Tabellla dei Contenuti", + "HeaderTableOfContents": "Tabella dei Contenuti", "HeaderTools": "Strumenti", "HeaderUpdateAccount": "Aggiorna Account", "HeaderUpdateAuthor": "Aggiorna Autore", @@ -181,11 +181,11 @@ "LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta", "LabelAddToPlaylist": "aggiungi alla Playlist", "LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist", - "LabelAdminUsersOnly": "Admin users only", + "LabelAdminUsersOnly": "Solo utenti Amministratori", "LabelAll": "Tutti", "LabelAllUsers": "Tutti gli Utenti", - "LabelAllUsersExcludingGuests": "All users excluding guests", - "LabelAllUsersIncludingGuests": "All users including guests", + "LabelAllUsersExcludingGuests": "Tutti gli Utenti Esclusi gli ospiti", + "LabelAllUsersIncludingGuests": "Tutti gli Utenti Inclusi gli ospiti", "LabelAlreadyInYourLibrary": "Già esistente nella libreria", "LabelAppend": "Appese", "LabelAuthor": "Autore", @@ -194,7 +194,7 @@ "LabelAuthors": "Autori", "LabelAutoDownloadEpisodes": "Auto Download Episodi", "LabelBackToUser": "Torna a Utenti", - "LabelBackupLocation": "Backup Location", + "LabelBackupLocation": "Percorso del Backup", "LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico", "LabelBackupsEnableAutomaticBackupsHelp": "I Backup saranno salvati in /metadata/backups", "LabelBackupsMaxBackupSize": "Dimensione massima backup (in GB)", @@ -208,11 +208,11 @@ "LabelChapters": "Capitoli", "LabelChaptersFound": "Capitoli Trovati", "LabelChapterTitle": "Titoli dei Capitoli", - "LabelClickForMoreInfo": "Click for more info", + "LabelClickForMoreInfo": "Click per altre Info", "LabelClosePlayer": "Chiudi player", "LabelCodec": "Codec", "LabelCollapseSeries": "Comprimi Serie", - "LabelCollection": "Collection", + "LabelCollection": "Raccolta", "LabelCollections": "Raccolte", "LabelComplete": "Completo", "LabelConfirmPassword": "Conferma Password", @@ -220,23 +220,23 @@ "LabelContinueReading": "Continua la Lettura", "LabelContinueSeries": "Continua Serie", "LabelCover": "Cover", - "LabelCoverImageURL": "Cover Image URL", + "LabelCoverImageURL": "Indirizzo della cover URL", "LabelCreatedAt": "Creato A", "LabelCronExpression": "Espressione Cron", "LabelCurrent": "Attuale", "LabelCurrently": "Attualmente:", - "LabelCustomCronExpression": "Custom Cron Expression:", + "LabelCustomCronExpression": "Espressione Cron personalizzata:", "LabelDatetime": "Data & Ora", - "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", + "LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (togli la spunta per eliminarla solo dal DB)", "LabelDescription": "Descrizione", "LabelDeselectAll": "Deseleziona Tutto", "LabelDevice": "Dispositivo", "LabelDeviceInfo": "Info Dispositivo", - "LabelDeviceIsAvailableTo": "Device is available to...", + "LabelDeviceIsAvailableTo": "Il dispositivo e disponibile su...", "LabelDirectory": "Elenco", "LabelDiscFromFilename": "Disco dal nome file", "LabelDiscFromMetadata": "Disco dal Metadata", - "LabelDiscover": "Discover", + "LabelDiscover": "Scopri", "LabelDownload": "Download", "LabelDownloadNEpisodes": "Download {0} episodes", "LabelDuration": "Durata", @@ -278,7 +278,7 @@ "LabelHost": "Host", "LabelHour": "Ora", "LabelIcon": "Icona", - "LabelImageURLFromTheWeb": "Image URL from the web", + "LabelImageURLFromTheWeb": "Immagine URL da internet", "LabelIncludeInTracklist": "Includi nella Tracklist", "LabelIncomplete": "Incompleta", "LabelInProgress": "In Corso", @@ -303,14 +303,14 @@ "LabelLastUpdate": "Ultimo Aggiornamento", "LabelLayout": "Layout", "LabelLayoutSinglePage": "Pagina Singola", - "LabelLayoutSplitPage": "DIvidi Pagina", + "LabelLayoutSplitPage": "Dividi Pagina", "LabelLess": "Poco", "LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti", "LabelLibrary": "Libreria", "LabelLibraryItem": "Elementi della Library", "LabelLibraryName": "Nome Libreria", "LabelLimit": "Limiti", - "LabelLineSpacing": "Line spacing", + "LabelLineSpacing": "Interlinea", "LabelListenAgain": "Ri-ascolta", "LabelLogLevelDebug": "Debug", "LabelLogLevelInfo": "Info", @@ -318,7 +318,7 @@ "LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Tipo Media", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "1 e bassa priorità, 5 è alta priorità", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", @@ -398,7 +398,7 @@ "LabelSeason": "Stagione", "LabelSelectAllEpisodes": "Seleziona tutti gli Episodi", "LabelSelectEpisodesShowing": "Episodi {0} selezionati ", - "LabelSelectUsers": "Select users", + "LabelSelectUsers": "Selezione Utenti", "LabelSendEbookToDevice": "Invia ebook a...", "LabelSequence": "Sequenza", "LabelSeries": "Serie", @@ -414,9 +414,9 @@ "LabelSettingsDisableWatcher": "Disattiva Watcher", "LabelSettingsDisableWatcherForLibrary": "Disattiva Watcher per le librerie", "LabelSettingsDisableWatcherHelp": "Disattiva il controllo automatico libri nelle cartelle delle librerie. *Richiede il Riavvio del Server", - "LabelSettingsEnableWatcher": "Enable Watcher", - "LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library", - "LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart", + "LabelSettingsEnableWatcher": "Abilita Watcher", + "LabelSettingsEnableWatcherForLibrary": "Abilita il controllo cartelle per la libreria", + "LabelSettingsEnableWatcherHelp": "Abilita l'aggiunta/aggiornamento automatico degli elementi quando vengono rilevate modifiche ai file. *Richiede il riavvio del Server", "LabelSettingsExperimentalFeatures": "Opzioni Sperimentali", "LabelSettingsExperimentalFeaturesHelp": "Funzionalità in fase di sviluppo che potrebbero utilizzare i tuoi feedback e aiutare i test. Fare clic per aprire la discussione github.", "LabelSettingsFindCovers": "Trova covers", @@ -471,8 +471,8 @@ "LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti", "LabelTasks": "Processi in esecuzione", "LabelTheme": "Tema", - "LabelThemeDark": "Dark", - "LabelThemeLight": "Light", + "LabelThemeDark": "Scuro", + "LabelThemeLight": "Chiaro", "LabelTimeBase": "Time Base", "LabelTimeListened": "Tempo di Ascolto", "LabelTimeListenedToday": "Tempo di Ascolto Oggi", @@ -532,21 +532,21 @@ "MessageChapterErrorStartLtPrev": "L'ora di inizio non valida deve essere maggiore o uguale all'ora di inizio del capitolo precedente", "MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro", "MessageCheckingCron": "Controllo cron...", - "MessageConfirmCloseFeed": "Are you sure you want to close this feed?", + "MessageConfirmCloseFeed": "Sei sicuro di voler chiudere questo feed?", "MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?", "MessageConfirmDeleteFile": "Questo eliminerà il file dal tuo file system. Sei sicuro?", "MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?", - "MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", - "MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItem": " l'elemento della libreria dal database e dal file system. Sei sicuro?", + "MessageConfirmDeleteLibraryItems": "Ciò eliminerà {0} elementi della libreria dal database e dal file system. Sei sicuro?", "MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?", "MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?", "MessageConfirmMarkAllEpisodesFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come finiti?", - "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", + "MessageConfirmMarkAllEpisodesNotFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come non completati?", "MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?", "MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?", - "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", + "MessageConfirmQuickEmbed": "Attenzione! L'incorporamento rapido non eseguirà il backup dei file audio. Assicurati di avere un backup dei tuoi file audio. <br><br>Vuoi Continuare?", "MessageConfirmRemoveAllChapters": "Sei sicuro di voler rimuovere tutti i capitoli?", - "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", + "MessageConfirmRemoveAuthor": "Sei sicuro di voler rimuovere l'autore? \"{0}\"?", "MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?", "MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?", "MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?", @@ -558,7 +558,7 @@ "MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?", "MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.", "MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".", - "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", + "MessageConfirmReScanLibraryItems": "Sei sicuro di voler ripetere la scansione? {0} oggetti?", "MessageConfirmSendEbookToDevice": "Sei sicuro di voler inviare {0} ebook \"{1}\" al Device \"{2}\"?", "MessageDownloadingEpisode": "Download episodio in corso", "MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto", @@ -608,7 +608,7 @@ "MessageNoResults": "Nessun Risultato", "MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"", "MessageNoSeries": "Nessuna Serie", - "MessageNoTags": "No Tags", + "MessageNoTags": "Nessun Tags", "MessageNoTasksRunning": "Nessun processo in esecuzione", "MessageNotYetImplemented": "Non Ancora Implementato", "MessageNoUpdateNecessary": "Nessun aggiornamento necessario", @@ -637,7 +637,7 @@ "MessageUploaderItemSuccess": "Caricato con successo!", "MessageUploading": "Caricamento...", "MessageValidCronExpression": "Espressione Cron Valida", - "MessageWatcherIsDisabledGlobally": "Watcher è disabilitato a livello globale nelle impostazioni del server", + "MessageWatcherIsDisabledGlobally": "Controllo file automatico è disabilitato a livello globale nelle impostazioni del server", "MessageXLibraryIsEmpty": "{0} libreria vuota!", "MessageYourAudiobookDurationIsLonger": "La durata dell'audiolibro è più lunga della durata trovata", "MessageYourAudiobookDurationIsShorter": "La durata dell'audiolibro è inferiore alla durata trovata", @@ -651,7 +651,7 @@ "NoteUploaderOnlyAudioFiles": "Se carichi solo file audio, ogni file audio verrà gestito come un audiolibro separato.", "NoteUploaderUnsupportedFiles": "I file non supportati vengono ignorati. Quando si sceglie o si elimina una cartella, gli altri file che non si trovano in una cartella di elementi vengono ignorati.", "PlaceholderNewCollection": "Nome Nuova Raccolta", - "PlaceholderNewFolderPath": "Nuovo percorso Cartella", + "PlaceholderNewFolderPath": "Nuovo Percorso Cartella", "PlaceholderNewPlaylist": "Nome nuova playlist", "PlaceholderSearch": "Cerca..", "PlaceholderSearchEpisode": "Cerca Episodio..", @@ -717,7 +717,7 @@ "ToastRSSFeedCloseSuccess": "RSS feed chiuso", "ToastSendEbookToDeviceFailed": "Impossibile inviare l'ebook al dispositivo", "ToastSendEbookToDeviceSuccess": "Ebook inviato al dispositivo \"{0}\"", - "ToastSeriesUpdateFailed": "Aggiornaento Serie Fallito", + "ToastSeriesUpdateFailed": "Aggiornamento Serie Fallito", "ToastSeriesUpdateSuccess": "Serie Aggornate", "ToastSessionDeleteFailed": "Errore eliminazione sessione", "ToastSessionDeleteSuccess": "Sessione cancellata", @@ -726,4 +726,4 @@ "ToastSocketFailedToConnect": "Socket non riesce a connettersi", "ToastUserDeleteFailed": "Errore eliminazione utente", "ToastUserDeleteSuccess": "Utente eliminato" -} \ No newline at end of file +} From e8c14dbb58e15482f9b2c50caf029cb3eddc72be Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 9 Nov 2023 19:58:51 +0000 Subject: [PATCH 141/208] Test BookFinder.js using mocha --- package-lock.json | 6395 ++++-------------------- package.json | 13 +- server/finders/bookFinder.test.js | 315 -- test/server/finders/BookFinder.test.js | 344 ++ 4 files changed, 1398 insertions(+), 5669 deletions(-) delete mode 100644 server/finders/bookFinder.test.js create mode 100644 test/server/finders/BookFinder.test.js diff --git a/package-lock.json b/package-lock.json index 650850c6f..c0f10bb8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,1039 +25,18 @@ "audiobookshelf": "prod.js" }, "devDependencies": { - "jest": "^29.7.0", - "nodemon": "^2.0.20" + "chai": "^4.3.10", + "mocha": "^10.2.0", + "nodemon": "^2.0.20", + "sinon": "^17.0.1" } }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", - "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", - "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/core/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", - "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/traverse/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -1144,12 +123,6 @@ "node": ">=10" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", @@ -1168,6 +141,32 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -1182,47 +181,6 @@ "node": ">= 6" } }, - "node_modules/@types/babel__core": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", - "integrity": "sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.6.tgz", - "integrity": "sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.3.tgz", - "integrity": "sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.3.tgz", - "integrity": "sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.7" - } - }, "node_modules/@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", @@ -1244,39 +202,6 @@ "@types/ms": "*" } }, - "node_modules/@types/graceful-fs": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.8.tgz", - "integrity": "sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz", - "integrity": "sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz", - "integrity": "sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, "node_modules/@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -1287,32 +212,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, - "node_modules/@types/stack-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz", - "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", - "dev": true - }, "node_modules/@types/validator": { "version": "13.7.17", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz", "integrity": "sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==" }, - "node_modules/@types/yargs": { - "version": "17.0.29", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", - "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", - "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==", - "dev": true - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1412,19 +316,13 @@ "node": ">=8" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/ansi-regex": { @@ -1480,20 +378,20 @@ "node": ">=10" } }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1508,122 +406,6 @@ "form-data": "^4.0.0" } }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1690,51 +472,10 @@ "node": ">=8" } }, - "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, "node_modules/bytes": { @@ -1786,44 +527,24 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, "engines": { - "node": ">=6" + "node": ">=4" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001561", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", - "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1861,13 +582,16 @@ "node": ">=8" } }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { - "node": ">=10" + "node": "*" } }, "node_modules/chokidar": { @@ -1905,27 +629,6 @@ "node": ">=10" } }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", - "dev": true - }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -1935,36 +638,6 @@ "node": ">=6" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2031,12 +704,6 @@ "node": ">= 0.6" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -2062,41 +729,6 @@ "node": ">= 0.10" } }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2105,27 +737,28 @@ "ms": "2.0.0" } }, - "node_modules/dedent": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", - "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", - "dev": true, - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" } }, "node_modules/delayed-stream": { @@ -2166,22 +799,13 @@ "node": ">=8" } }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true, "engines": { - "node": ">=8" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.3.1" } }, "node_modules/dom-serializer": { @@ -2245,24 +869,6 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, - "node_modules/electron-to-chromium": { - "version": "1.4.576", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz", - "integrity": "sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==", - "dev": true - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2380,15 +986,6 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "optional": true }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2403,28 +1000,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -2433,54 +1008,6 @@ "node": ">= 0.6" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -2522,21 +1049,6 @@ "node": ">= 0.10.0" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "dependencies": { - "bser": "2.1.1" - } - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2566,17 +1078,13 @@ "node": ">= 0.8" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" + "bin": { + "flat": "cli.js" } }, "node_modules/follow-redirects": { @@ -2684,15 +1192,6 @@ "node": ">=10" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2702,6 +1201,15 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -2715,27 +1223,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2767,15 +1254,6 @@ "node": ">= 6" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -2817,24 +1295,15 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, - "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" + "bin": { + "he": "bin/he" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, "node_modules/htmlparser2": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", @@ -2944,15 +1413,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -2979,30 +1439,11 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "devOptional": true, + "optional": true, "engines": { "node": ">=0.8.19" } @@ -3058,12 +1499,6 @@ "node": ">= 0.10" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3076,18 +1511,6 @@ "node": ">=8" } }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3105,15 +1528,6 @@ "node": ">=8" } }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3141,848 +1555,81 @@ "node": ">=0.12.0" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true, "engines": { "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true + "optional": true }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", - "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/istanbul-lib-report/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, - "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4043,15 +1690,6 @@ "node": ">= 10" } }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "dependencies": { - "tmpl": "1.0.5" - } - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -4065,12 +1703,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -4079,19 +1711,6 @@ "node": ">= 0.6" } }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -4122,15 +1741,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4241,6 +1851,266 @@ "node": ">=10" } }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/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==", + "dev": true + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/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==", + "dev": true + }, + "node_modules/mocha/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -4265,11 +2135,17 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, "node_modules/negotiator": { "version": "0.6.3", @@ -4279,6 +2155,37 @@ "node": ">= 0.6" } }, + "node_modules/nise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/node-addon-api": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", @@ -4404,18 +2311,6 @@ "node": ">=10" } }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", - "dev": true - }, "node_modules/node-tone": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz", @@ -4496,18 +2391,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -4554,21 +2437,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4584,33 +2452,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -4626,33 +2467,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -4678,37 +2492,25 @@ "node": ">=0.10.0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/pg-connection-string": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -4721,53 +2523,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -4787,19 +2542,6 @@ "node": ">=10" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4818,22 +2560,6 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, - "node_modules/pure-rand": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", - "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ] - }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -4848,6 +2574,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4870,12 +2605,6 @@ "node": ">= 0.8" } }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -4910,53 +2639,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -5155,6 +2837,15 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -5179,27 +2870,6 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -5239,17 +2909,59 @@ "semver": "bin/semver.js" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { "node": ">=8" } @@ -5390,31 +3102,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, "node_modules/sqlite3": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", @@ -5465,18 +3152,6 @@ "node": ">= 8" } }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -5493,19 +3168,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5530,24 +3192,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5572,18 +3216,6 @@ "node": ">=4" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/tar": { "version": "6.1.15", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", @@ -5608,35 +3240,6 @@ "node": ">=8" } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5688,18 +3291,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -5744,36 +3335,6 @@ "node": ">= 0.8" } }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5795,20 +3356,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/v8-to-istanbul": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", - "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/validator": { "version": "13.9.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", @@ -5825,15 +3372,6 @@ "node": ">= 0.8" } }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -5852,7 +3390,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, + "optional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -5879,6 +3417,12 @@ "@types/node": "*" } }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -5901,19 +3445,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/ws": { "version": "8.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", @@ -5968,31 +3499,31 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" }, "engines": { - "node": ">=12" + "node": ">=10" } }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/yocto-queue": { @@ -6009,802 +3540,12 @@ } }, "dependencies": { - "@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - } - } - }, - "@babel/compat-data": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", - "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", - "dev": true - }, - "@babel/core": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", - "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", - "dev": true, - "requires": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "dependencies": { - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true - }, - "@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "requires": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "requires": { - "@babel/types": "^7.22.15" - } - }, - "@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true - }, - "@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", - "dev": true - }, - "@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", - "dev": true, - "requires": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" - } - }, - "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - } - } - }, - "@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", - "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - } - }, - "@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - } - }, - "@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "requires": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "requires": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - } - }, - "@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "requires": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - } - }, - "@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "requires": { - "jest-get-type": "^29.6.3" - } - }, - "@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - } - }, - "@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "requires": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - } - }, - "@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - } - }, - "@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.27.8" - } - }, - "@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - } - }, - "@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "requires": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "requires": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - } - }, - "@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - } - }, - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -6870,12 +3611,6 @@ "rimraf": "^3.0.2" } }, - "@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", @@ -6894,6 +3629,34 @@ "@sinonjs/commons": "^3.0.0" } }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -6905,47 +3668,6 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "optional": true }, - "@types/babel__core": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", - "integrity": "sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==", - "dev": true, - "requires": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.6.tgz", - "integrity": "sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.3.tgz", - "integrity": "sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.3.tgz", - "integrity": "sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==", - "dev": true, - "requires": { - "@babel/types": "^7.20.7" - } - }, "@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", @@ -6967,39 +3689,6 @@ "@types/ms": "*" } }, - "@types/graceful-fs": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.8.tgz", - "integrity": "sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz", - "integrity": "sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz", - "integrity": "sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, "@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -7010,32 +3699,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, - "@types/stack-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz", - "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", - "dev": true - }, "@types/validator": { "version": "13.7.17", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz", "integrity": "sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==" }, - "@types/yargs": { - "version": "17.0.29", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", - "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", - "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==", - "dev": true - }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -7111,14 +3779,11 @@ "indent-string": "^4.0.0" } }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - } + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true }, "ansi-regex": { "version": "5.0.1", @@ -7158,20 +3823,17 @@ "readable-stream": "^3.6.0" } }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -7186,97 +3848,6 @@ "form-data": "^4.0.0" } }, - "babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "requires": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - } - }, - "babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "dependencies": { - "istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - } - }, - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - } - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -7330,31 +3901,10 @@ "fill-range": "^7.0.1" } }, - "browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" - } - }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, "bytes": { @@ -7397,23 +3947,20 @@ "get-intrinsic": "^1.0.2" } }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001561", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", - "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", - "dev": true + "chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } }, "chalk": { "version": "4.1.2", @@ -7442,11 +3989,14 @@ } } }, - "char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } }, "chokidar": { "version": "3.5.3", @@ -7469,47 +4019,12 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" }, - "ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true - }, - "cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", - "dev": true - }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "optional": true }, - "cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true - }, - "collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -7561,12 +4076,6 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, - "convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, "cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -7586,32 +4095,6 @@ "vary": "^1" } }, - "create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7620,19 +4103,21 @@ "ms": "2.0.0" } }, - "dedent": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", - "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", - "dev": true, - "requires": {} - }, - "deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7658,16 +4143,10 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, "dom-serializer": { @@ -7713,18 +4192,6 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, - "electron-to-chromium": { - "version": "1.4.576", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz", - "integrity": "sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==", - "dev": true - }, - "emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -7814,15 +4281,6 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "optional": true }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -7834,59 +4292,11 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true - }, - "expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "requires": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - } - }, "express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -7925,21 +4335,6 @@ "vary": "~1.1.2" } }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -7963,15 +4358,11 @@ "unpipe": "~1.0.0" } }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true }, "follow-redirects": { "version": "1.15.2", @@ -8039,18 +4430,18 @@ "wide-align": "^1.1.2" } }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, "get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -8061,18 +4452,6 @@ "has-symbols": "^1.0.3" } }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8095,12 +4474,6 @@ "is-glob": "^4.0.1" } }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -8130,19 +4503,10 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, - "hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, - "requires": { - "function-bind": "^1.1.2" - } - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, "htmlparser2": { @@ -8226,12 +4590,6 @@ } } }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true - }, "humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -8255,21 +4613,11 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, - "import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "devOptional": true + "optional": true }, "indent-string": { "version": "4.0.0", @@ -8313,12 +4661,6 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -8328,15 +4670,6 @@ "binary-extensions": "^2.0.0" } }, - "is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "requires": { - "hasown": "^2.0.0" - } - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8348,12 +4681,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8375,641 +4702,66 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "dev": true }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true + "optional": true }, - "istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, - "istanbul-lib-instrument": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", - "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "dependencies": { - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "requires": { - "semver": "^7.5.3" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "requires": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - } - }, - "jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "requires": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - } - }, - "jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "requires": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - } - }, - "jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "requires": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - } - }, - "jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - } - }, - "jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - } - }, - "jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - } - }, - "jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "requires": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - } - }, - "jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true - }, - "jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - } - }, - "jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "requires": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - } - }, - "jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - } - }, - "jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - } - }, - "jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - } - }, - "jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "requires": {} - }, - "jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true - }, - "jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - } - }, - "jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "requires": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - } - }, - "jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "requires": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - } - }, - "jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "requires": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - } - }, - "jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "dependencies": { - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - } - }, - "jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - } - } - }, - "jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "requires": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - } - }, - "jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "requires": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "requires": { + "get-func-name": "^2.0.1" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -9057,15 +4809,6 @@ "ssri": "^8.0.0" } }, - "makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "requires": { - "tmpl": "1.0.5" - } - }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -9076,27 +4819,11 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -9115,12 +4842,6 @@ "mime-db": "1.52.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -9199,6 +4920,201 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, + "mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + } + } + }, "moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -9217,10 +5133,10 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", "dev": true }, "negotiator": { @@ -9228,6 +5144,39 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, + "nise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "node-addon-api": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", @@ -9317,18 +5266,6 @@ } } }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, - "node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", - "dev": true - }, "node-tone": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz", @@ -9389,15 +5326,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, "npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -9435,15 +5363,6 @@ "wrappy": "1" } }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9453,26 +5372,6 @@ "yocto-queue": "^0.1.0" } }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - }, - "dependencies": { - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - } - } - }, "p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -9482,24 +5381,6 @@ "aggregate-error": "^3.0.0" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -9516,74 +5397,28 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, "pg-connection-string": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, - "pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -9600,16 +5435,6 @@ "retry": "^0.12.0" } }, - "prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -9625,12 +5450,6 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, - "pure-rand": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", - "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", - "dev": true - }, "qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -9639,6 +5458,15 @@ "side-channel": "^1.0.4" } }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -9655,12 +5483,6 @@ "unpipe": "1.0.0" } }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -9686,38 +5508,6 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, - "resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "requires": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "dev": true - }, "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -9836,6 +5626,15 @@ "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==" }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -9857,21 +5656,6 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -9904,17 +5688,51 @@ } } }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true + "sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } }, "smart-buffer": { "version": "4.2.0", @@ -10017,28 +5835,6 @@ } } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, "sqlite3": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", @@ -10074,15 +5870,6 @@ "minipass": "^3.1.1" } }, - "stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - } - }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -10096,16 +5883,6 @@ "safe-buffer": "~5.2.0" } }, - "string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "requires": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - } - }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -10124,18 +5901,6 @@ "ansi-regex": "^5.0.1" } }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10151,12 +5916,6 @@ "has-flag": "^3.0.0" } }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, "tar": { "version": "6.1.15", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", @@ -10177,29 +5936,6 @@ } } }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10239,12 +5975,6 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -10283,16 +6013,6 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, - "update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10308,17 +6028,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, - "v8-to-istanbul": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", - "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - } - }, "validator": { "version": "13.9.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", @@ -10329,15 +6038,6 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, - "walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "requires": { - "makeerror": "1.0.12" - } - }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -10356,7 +6056,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -10377,6 +6077,12 @@ "@types/node": "*" } }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -10393,16 +6099,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - } - }, "ws": { "version": "8.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", @@ -10434,27 +6130,26 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } } }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true - }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index c21a12f6c..62e1eb2e1 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "docker-arm64-local": "docker buildx build --platform linux/arm64 --load . -t advplyr/audiobookshelf-arm64-local", "docker-armv7-local": "docker buildx build --platform linux/arm/v7 --load . -t advplyr/audiobookshelf-armv7-local", "deploy-linux": "node deploy/linux", - "test": "jest" + "test": "mocha" }, "bin": "prod.js", "pkg": { @@ -29,6 +29,9 @@ "server/**/*.js" ] }, + "mocha": { + "recursive": true + }, "author": "advplyr", "license": "GPL-3.0", "dependencies": { @@ -45,7 +48,9 @@ "xml2js": "^0.5.0" }, "devDependencies": { - "jest": "^29.7.0", - "nodemon": "^2.0.20" + "chai": "^4.3.10", + "mocha": "^10.2.0", + "nodemon": "^2.0.20", + "sinon": "^17.0.1" } -} \ No newline at end of file +} diff --git a/server/finders/bookFinder.test.js b/server/finders/bookFinder.test.js deleted file mode 100644 index 2c54c8804..000000000 --- a/server/finders/bookFinder.test.js +++ /dev/null @@ -1,315 +0,0 @@ -const bookFinder = require('./BookFinder') -const Audnexus = require('../providers/Audnexus') -const { LogLevel } = require('../utils/constants') -const Logger = require('../Logger') -jest.mock('../providers/Audnexus') - -Logger.setLogLevel(LogLevel.INFO) - -describe('TitleCandidates', () => { - describe('cleanAuthor non-empty', () => { - let titleCandidates - let cleanAuthor = 'leo tolstoy' - - beforeEach(() => { - titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) - }) - - describe('single add', () => { - it.each([ - ['adds a clean title to candidates', 'anna karenina', ['anna karenina']], - ['lowercases candidate title', 'ANNA KARENINA', ['anna karenina']], - ['removes author name from title', `anna karenina by ${cleanAuthor}`, ['anna karenina']], - ['removes author name title', cleanAuthor, []], - ['cleans subtitle from title', 'anna karenina: subtitle', ['anna karenina']], - ['removes "by ..." from title', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], - ['removes bitrate from title', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], - ['removes edition from title 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], - ['removes edition from title 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], - ['removes file-type from title', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']], - ['removes "a novel" from title', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']], - ['removes preceding/trailing numbers from title', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']], - ['does not add empty title', '', []], - ['does not add title with only spaces', ' ', []], - ['adds digit-only title, but not its empty string transformation', '1984', ['1984']], - ])('%s', (_, title, expected) => { - titleCandidates.add(title) - expect(titleCandidates.getCandidates()).toEqual(expected) - }) - }) - - describe('multi add', () => { - it.each([ - ['digits-only candidates get lower priority', ['01', 'anna karenina'], ['anna karenina', '01']], - ['transformed candidates get higher priority', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']], - ['other candidates are ordered by position', ['title1', 'title2'], ['title1', 'title2']], - ['author candidate is removed', ['title1', cleanAuthor], ['title1']], - ])('%s', (_, titles, expected) => { - for (const title of titles) titleCandidates.add(title) - expect(titleCandidates.getCandidates()).toEqual(expected) - }) - }) - }) - - describe('cleanAuthor empty', () => { - let titleCandidates - let cleanAuthor = '' - - beforeEach(() => { - titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) - }) - - describe('single add', () => { - it.each([ - ['does not remove author name', 'leo tolstoy', ['leo tolstoy']], - ])('%s', (_, title, expected) => { - titleCandidates.add(title) - expect(titleCandidates.getCandidates()).toEqual(expected) - }) - }) - }) -}) - - -describe('AuthorCandidates', () => { - let authorCandidates - const audnexus = new Audnexus() - audnexus.authorASINsRequest.mockResolvedValue([ - { name: 'Leo Tolstoy' }, - { name: 'Nikolai Gogol' }, - { name: 'J. K. Rowling' }, - ]) - - describe('cleanAuthor is null', () => { - beforeEach(() => { - authorCandidates = new bookFinder.constructor.AuthorCandidates(null, audnexus) - }) - - describe('no add', () => { - it.each([ - ['returns empty author', []], - ])('%s', async (_, expected) => { - expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) - }) - }) - - describe('single add', () => { - it.each([ - ['returns valid author', 'nikolai gogol', ['nikolai gogol']], - ['does not return invalid author (not in list)', 'fyodor dostoevsky', []], - ['returns valid author (valid is a substring of added)', 'dr. nikolai gogol', ['nikolai gogol']], - ['returns added author (added is a substring of valid)', 'gogol', ['gogol']], - ['returns valid author (added is similar to valid)', 'nicolai gogol', ['nikolai gogol']], - ['does not return invalid author (added too distant)', 'nikolai google', []], - ['returns valid author (contains redundant spaces)', 'nikolai gogol', ['nikolai gogol']], - ['returns valid author (normalized initials)', 'j.k. rowling', ['j. k. rowling']], - ])('%s', async (_, author, expected) => { - authorCandidates.add(author) - expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) - }) - }) - - describe('multi add', () => { - it.each([ - ['returns valid authors', ['nikolai gogol', 'leo tolstoy'], ['nikolai gogol', 'leo tolstoy']], - ['returns deduped valid authors', ['nikolai gogol', 'nikolai gogol'], ['nikolai gogol']], - ])('%s', async (_, authors, expected) => { - for (const author of authors) authorCandidates.add(author) - expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) - }) - }) - }) - - describe('cleanAuthor is valid', () => { - const cleanAuthor = 'leo tolstoy' - - beforeEach(() => { - authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) - }) - - describe('no add', () => { - it.each([ - ['returns clean author from constructor', [cleanAuthor]], - ])('%s', async (_, expected) => { - expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) - }) - }) - - describe('single add', () => { - it.each([ - ['returns cleanAuthor + valid author', 'nikolai gogol', [cleanAuthor, 'nikolai gogol']], - ['returns deduplicated author', cleanAuthor, [cleanAuthor]], - ])('%s', async (_, author, expected) => { - authorCandidates.add(author) - expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) - }) - }) - }) - - - describe('cleanAuthor is invalid', () => { - const cleanAuthor = 'fyodor dostoevsky' - - beforeEach(() => { - authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) - }) - - describe('no add', () => { - it.each([ - ['returns invalid clean author from constructor', [cleanAuthor]], - ])('%s', async (_, expected) => { - expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) - }) - }) - - describe('single add', () => { - it.each([ - ['returns only valid author', 'nikolai gogol', ['nikolai gogol']], - ])('%s', async (_, author, expected) => { - authorCandidates.add(author) - expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) - }) - }) - }) - - describe('cleanAuthor is invalid and dirty', () => { - describe('no add', () => { - it.each([ - ['returns invalid aggressively cleanAuthor from constructor', 'fyodor dostoevsky, translated by jackie chan', ['fyodor dostoevsky']], - ['returns invalid cleanAuthor from constructor (empty after aggressive ckean)', ', jackie chan', [', jackie chan']], - ])('%s', async (_, cleanAuthor, expected) => { - authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) - expect(await authorCandidates.getCandidates()).toEqual([...expected, '']) - }) - }) - }) -}) - -describe('search', () => { - const t = 'title' - const a = 'author' - const u = 'unknown' - const r = ['book'] - - bookFinder.runSearch = jest.fn((searchTitle, searchAuthor) => { - return new Promise((resolve) => { - resolve(searchTitle == t && (searchAuthor == a || searchAuthor == u) ? r : []) - }) - }) - - const audnexus = new Audnexus() - audnexus.authorASINsRequest.mockResolvedValue([ - { name: a }, - ]) - bookFinder.audnexus = audnexus - - beforeEach(() => { - bookFinder.runSearch.mockClear() - }) - - describe('no or empty title', () => { - it('returns empty result', async () => { - expect(await bookFinder.search('', '', a)).toEqual([]) - expect(bookFinder.runSearch).toHaveBeenCalledTimes(0) - }) - }) - - describe('exact valid title and exact valid author', () => { - it('returns result (no fuzzy searches)', async () => { - expect(await bookFinder.search('', t, a)).toEqual(r) - expect(bookFinder.runSearch).toHaveBeenCalledTimes(1) - }) - }) - - describe('contains valid title and exact valid author', () => { - it.each([ - [`${t} -`], - [`${t} - ${a}`], - [`${a} - ${t}`], - [`${t}- ${a}`], - [`${t} -${a}`], - [`${t} ${a}`], - [`${a} - ${t} (unabridged)`], - [`${a} - ${t} (subtitle) - mp3`], - [`${t} {narrator} - series-01 64kbps 10:00:00`], - [`${a} - ${t} (2006) narrated by narrator [unabridged]`], - [`${t} - ${a} 2022 mp3`], - [`01 ${t}`], - [`2022_${t}_HQ`], - ])(`returns result ('%s', '${a}') (1 fuzzy search)` , async (searchTitle) => { - expect(await bookFinder.search('', searchTitle, a)).toEqual(r) - expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) - }) - - - it.each([ - [`s-01 - ${t} (narrator) 64kbps 10:00:00`], - [`${a} - series 01 - ${t}`], - ])(`returns result ('%s', '${a}') (2 fuzzy searches)` , async (searchTitle) => { - expect(await bookFinder.search('', searchTitle, a)).toEqual(r) - expect(bookFinder.runSearch).toHaveBeenCalledTimes(3) - }) - - it.each([ - [`${t}-${a}`], - [`${t} junk`], - ])(`returns empty result ('%s', '${a}')`, async (searchTitle) => { - expect(await bookFinder.search('', searchTitle, a)).toEqual([]) - }) - - describe('maxFuzzySearches = 0', () => { - it.each([ - [`${t} - ${a}`], - ])(`returns empty result ('%s', '${a}') (no fuzzy search)` , async (searchTitle) => { - expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 0 })).toEqual([]) - expect(bookFinder.runSearch).toHaveBeenCalledTimes(1) - }) - }) - - describe('maxFuzzySearches = 1', () => { - it.each([ - [`s-01 - ${t} (narrator) 64kbps 10:00:00`], - [`${a} - series 01 - ${t}`], - ])(`returns empty result ('%s', '${a}') (1 fuzzy search)` , async (searchTitle) => { - expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 1 })).toEqual([]) - expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) - }) - }) - }) - - describe('contains valid title and no author', () => { - it.each([ - [`${t} - ${a}`], - [`${a} - ${t}`], - ])(`returns result ('%s', '') (1 fuzzy search)` , async (searchTitle) => { - expect(await bookFinder.search('', searchTitle, '')).toEqual(r) - expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) - }) - - it.each([ - [`${t}`], - [`${t} - ${u}`], - [`${u} - ${t}`], - ])(`returns empty result ('%s', '') (no fuzzy search)` , async (searchTitle) => { - expect(await bookFinder.search('', searchTitle, '')).toEqual([]) - }) - }) - - describe('contains valid title and unknown author', () => { - it.each([ - [`${t} - ${u}`], - [`${u} - ${t}`], - ])(`returns result ('%s', '') (1 fuzzy search)` , async (searchTitle) => { - expect(await bookFinder.search('', searchTitle, u)).toEqual(r) - expect(bookFinder.runSearch).toHaveBeenCalledTimes(2) - }) - - it.each([ - [`${t}`], - ])(`returns result ('%s', '') (no fuzzy search)` , async (searchTitle) => { - expect(await bookFinder.search('', searchTitle, u)).toEqual(r) - expect(bookFinder.runSearch).toHaveBeenCalledTimes(1) - }) - }) - -}) \ No newline at end of file diff --git a/test/server/finders/BookFinder.test.js b/test/server/finders/BookFinder.test.js new file mode 100644 index 000000000..01dcb5758 --- /dev/null +++ b/test/server/finders/BookFinder.test.js @@ -0,0 +1,344 @@ +const sinon = require('sinon'); +const chai = require('chai'); +const expect = chai.expect; +const bookFinder = require('../../../server/finders/BookFinder'); +const { LogLevel } = require('../../../server/utils/constants') +const Logger = require('../../../server/Logger') +Logger.setLogLevel(LogLevel.INFO) + + describe('TitleCandidates', () => { + describe('cleanAuthor non-empty', () => { + let titleCandidates; + const cleanAuthor = 'leo tolstoy'; + + beforeEach(() => { + titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor); + }); + + describe('no adds', () => { + it('returns no candidates', () => { + expect(titleCandidates.getCandidates()).to.deep.equal([]); + }) + }) + + describe('single add', () => { + [ + ['adds candidate', 'anna karenina', ['anna karenina']], + ['adds lowercased candidate', 'ANNA KARENINA', ['anna karenina']], + ['adds candidate, removing redundant spaces', 'anna karenina', ['anna karenina']], + ['adds candidate, removing author', `anna karenina by ${cleanAuthor}`, ['anna karenina']], + ['does not add empty candidate after removing author', cleanAuthor, []], + ['adds candidate, removing subtitle', 'anna karenina: subtitle', ['anna karenina']], + ['adds candidate + variant, removing "by ..."', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], + ['adds candidate + variant, removing bitrate', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], + ['adds candidate + variant, removing edition 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], + ['adds candidate + variant, removing edition 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], + ['adds candidate + variant, removing fie type', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']], + ['adds candidate + variant, removing "a novel"', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']], + ['adds candidate + variant, removing preceding/trailing numbers', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']], + ['does not add empty candidate', '', []], + ['does not add spaces-only candidate', ' ', []], + ['does not add empty variant', '1984', ['1984']], + ].forEach(([name, title, expected]) => it(name, () => { + titleCandidates.add(title); + expect(titleCandidates.getCandidates()).to.deep.equal(expected); + })); + }) + + describe('multiple adds', () => { + [ + ['demotes digits-only candidates', ['01', 'anna karenina'], ['anna karenina', '01']], + ['promotes transformed variants', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']], + ['orders by position', ['title2', 'title1'], ['title2', 'title1']], + ['dedupes candidates', ['title1', 'title1'], ['title1']], + ].forEach(([name, titles, expected]) => it(name, () => { + for (const title of titles) titleCandidates.add(title) + expect(titleCandidates.getCandidates()).to.deep.equal(expected); + })); + }) + }) + + describe('cleanAuthor empty', () => { + let titleCandidates + let cleanAuthor = '' + + beforeEach(() => { + titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) + }) + + describe('single add', () => { + [ + ['adds a candidate', 'leo tolstoy', ['leo tolstoy']], + ].forEach(([name, title, expected]) => it(name, () => { + titleCandidates.add(title) + expect(titleCandidates.getCandidates()).to.deep.equal(expected); + })) + }) + }) + }) + + describe('AuthorCandidates', () => { + let authorCandidates; + const audnexus = { + authorASINsRequest: sinon.stub().resolves([ + { name: 'Leo Tolstoy' }, + { name: 'Nikolai Gogol' }, + { name: 'J. K. Rowling' }, + ]), + }; + + describe('cleanAuthor is null', () => { + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(null, audnexus); + }); + + describe('no adds', () => { + [ + ['returns empty author candidate', []], + ].forEach(([name, expected]) => it(name, async () => { + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }); + + describe('single add', () => { + [ + ['adds recognized candidate', 'nikolai gogol', ['nikolai gogol']], + ['does not add unrecognized candidate', 'fyodor dostoevsky', []], + ['adds recognized author if candidate is a superstring', 'dr. nikolai gogol', ['nikolai gogol']], + ['adds candidate if it is a substring of recognized author', 'gogol', ['gogol']], + ['adds recognized author if edit distance from candidate is small', 'nicolai gogol', ['nikolai gogol']], + ['does not add candidate if edit distance from any recognized author is large', 'nikolai google', []], + ['adds normalized recognized candidate (contains redundant spaces)', 'nikolai gogol', ['nikolai gogol']], + ['adds normalized recognized candidate (normalized initials)', 'j.k. rowling', ['j. k. rowling']], + ].forEach(([name, author, expected]) => it(name, async () => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })); + }) + + describe('multi add', () => { + [ + ['adds recognized author candidates', ['nikolai gogol', 'leo tolstoy'], ['nikolai gogol', 'leo tolstoy']], + ['dedupes author candidates', ['nikolai gogol', 'nikolai gogol'], ['nikolai gogol']], + ].forEach(([name, authors, expected]) => it(name, async () => { + for (const author of authors) authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }); + + describe('cleanAuthor is a recognized author', () => { + const cleanAuthor = 'leo tolstoy'; + + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus); + }); + + describe('no adds', () => { + [ + ['adds cleanAuthor as candidate', [cleanAuthor]], + ].forEach(([name, expected]) => it(name, async () => { + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('single add', () => { + [ + ['adds recognized candidate', 'nikolai gogol', [cleanAuthor, 'nikolai gogol']], + ['does not add candidate if it is a dupe of cleanAuthor', cleanAuthor, [cleanAuthor]], + ].forEach(([name, author, expected]) => it(name, async () => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }); + + describe('cleanAuthor is an unrecognized author', () => { + const cleanAuthor = 'Fyodor Dostoevsky'; + + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus); + }); + + describe('no adds', () => { + [ + ['adds cleanAuthor as candidate', [cleanAuthor]], + ].forEach(([name, expected]) => it(name, async () => { + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('single add', () => { + [ + ['adds recognized candidate and removes cleanAuthor', 'nikolai gogol', ['nikolai gogol']], + ['does not add unrecognized candidate', 'jackie chan', [cleanAuthor]], + ].forEach(([name, author, expected]) => it(name, async () => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }); + + describe('cleanAuthor is unrecognized and dirty', () => { + describe('no adds', () => { + [ + ['adds aggressively cleaned cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', ['fyodor dostoevsky']], + ['adds cleanAuthor if aggresively cleaned cleanAuthor is empty', ', jackie chan', [', jackie chan']], + ].forEach(([name, cleanAuthor, expected]) => it(name, async () => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('single add', () => { + [ + ['adds recognized candidate and removes cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', 'nikolai gogol', ['nikolai gogol']], + ].forEach(([name, cleanAuthor, author, expected]) => it(name, async () => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }); + }); + + describe('search', () => { + const t = 'title'; + const a = 'author'; + const u = 'unrecognized'; + const r = ['book']; + + const runSearchStub = sinon.stub(bookFinder, 'runSearch') + runSearchStub.resolves([]) + runSearchStub.withArgs(t, a).resolves(r); + runSearchStub.withArgs(t, u).resolves(r); + + const audnexusStub = sinon.stub(bookFinder.audnexus, 'authorASINsRequest') + audnexusStub.resolves([ { name: a } ]) + + beforeEach(() => { + bookFinder.runSearch.resetHistory(); + }); + + describe('search title is empty', () => { + it('returns empty result', async () => { + expect(await bookFinder.search('', '', a)).to.deep.equal([]); + sinon.assert.callCount(bookFinder.runSearch, 0); + }); + }); + + describe('search title is a recognized title and search author is a recognized author', () => { + it('returns non-empty result (no fuzzy searches)', async () => { + expect(await bookFinder.search('', t, a)).to.deep.equal(r); + sinon.assert.callCount(bookFinder.runSearch, 1); + }); + }); + + describe('search title contains recognized title and search author is a recognized author', () => { + [ + [`${t} -`], + [`${t} - ${a}`], + [`${a} - ${t}`], + [`${t}- ${a}`], + [`${t} -${a}`], + [`${t} ${a}`], + [`${a} - ${t} (unabridged)`], + [`${a} - ${t} (subtitle) - mp3`], + [`${t} {narrator} - series-01 64kbps 10:00:00`], + [`${a} - ${t} (2006) narrated by narrator [unabridged]`], + [`${t} - ${a} 2022 mp3`], + [`01 ${t}`], + [`2022_${t}_HQ`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns non-empty result (with 1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r); + sinon.assert.callCount(bookFinder.runSearch, 2); + }); + }); + + [ + [`s-01 - ${t} (narrator) 64kbps 10:00:00`], + [`${a} - series 01 - ${t}`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns non-empty result (with 2 fuzzy searches)`, async () => { + expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r); + sinon.assert.callCount(bookFinder.runSearch, 3); + }); + }); + + [ + [`${t}-${a}`], + [`${t} junk`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns an empty result`, async () => { + expect(await bookFinder.search('', searchTitle, a)).to.deep.equal([]); + }); + }); + + describe('maxFuzzySearches = 0', () => { + [ + [`${t} - ${a}`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns an empty result (with no fuzzy searches)`, async () => { + expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 0 })).to.deep.equal([]); + sinon.assert.callCount(bookFinder.runSearch, 1); + }); + }); + }); + + describe('maxFuzzySearches = 1', () => { + [ + [`s-01 - ${t} (narrator) 64kbps 10:00:00`], + [`${a} - series 01 - ${t}`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns an empty result (1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 1 })).to.deep.equal([]); + sinon.assert.callCount(bookFinder.runSearch, 2); + }); + }); + }); + }); + + describe('search title contains recognized title and search author is empty', () => { + [ + [`${t} - ${a}`], + [`${a} - ${t}`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '') returns a non-empty result (1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, '')).to.deep.equal(r); + sinon.assert.callCount(bookFinder.runSearch, 2); + }); + }); + + [ + [`${t}`], + [`${t} - ${u}`], + [`${u} - ${t}`] + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '') returns an empty result`, async () => { + expect(await bookFinder.search('', searchTitle, '')).to.deep.equal([]); + }); + }); + }); + + describe('search title contains recognized title and search author is an unrecognized author', () => { + [ + [`${t} - ${u}`], + [`${u} - ${t}`] + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${u}') returns a non-empty result (1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r); + sinon.assert.callCount(bookFinder.runSearch, 2); + }); + }); + + [ + [`${t}`] + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${u}') returns a non-empty result (no fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r); + sinon.assert.callCount(bookFinder.runSearch, 1); + }); + }); + }); + }); From 33e287a543c2edbe7d6edb8e948b0afcfdc26cf5 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 9 Nov 2023 16:26:49 -0600 Subject: [PATCH 142/208] Update:Persist show full path option for tables #2285 --- client/components/modals/item/tabs/Files.vue | 3 +-- client/components/tables/EbookFilesTable.vue | 12 ++++++++++-- client/components/tables/LibraryFilesTable.vue | 9 ++++++++- client/components/tables/TracksTable.vue | 12 ++++++++++-- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/client/components/modals/item/tabs/Files.vue b/client/components/modals/item/tabs/Files.vue index 4081f98cb..7be286fef 100644 --- a/client/components/modals/item/tabs/Files.vue +++ b/client/components/modals/item/tabs/Files.vue @@ -14,8 +14,7 @@ export default { }, data() { return { - tracks: [], - showFullPath: false + tracks: [] } }, watch: { diff --git a/client/components/tables/EbookFilesTable.vue b/client/components/tables/EbookFilesTable.vue index 0c85774c4..3532c2a7e 100644 --- a/client/components/tables/EbookFilesTable.vue +++ b/client/components/tables/EbookFilesTable.vue @@ -6,7 +6,7 @@ <span class="text-sm font-mono">{{ ebookFiles.length }}</span> </div> <div class="flex-grow" /> - <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $strings.ButtonFullPath }}</ui-btn> + <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''"> <span class="material-icons text-4xl">expand_more</span> </div> @@ -75,6 +75,10 @@ export default { } }, methods: { + toggleFullPath() { + this.showFullPath = !this.showFullPath + localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0) + }, readEbook(fileIno) { this.$store.commit('showEReader', { libraryItem: this.libraryItem, keepProgress: false, fileId: fileIno }) }, @@ -82,6 +86,10 @@ export default { this.showFiles = !this.showFiles } }, - mounted() {} + mounted() { + if (this.userIsAdmin) { + this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0) + } + } } </script> \ No newline at end of file diff --git a/client/components/tables/LibraryFilesTable.vue b/client/components/tables/LibraryFilesTable.vue index c6c8c7774..fef1ae5ae 100644 --- a/client/components/tables/LibraryFilesTable.vue +++ b/client/components/tables/LibraryFilesTable.vue @@ -6,7 +6,7 @@ <span class="text-sm font-mono">{{ files.length }}</span> </div> <div class="flex-grow" /> - <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $strings.ButtonFullPath }}</ui-btn> + <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''"> <span class="material-icons text-4xl">expand_more</span> </div> @@ -84,6 +84,10 @@ export default { } }, methods: { + toggleFullPath() { + this.showFullPath = !this.showFullPath + localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0) + }, clickBar() { this.showFiles = !this.showFiles }, @@ -93,6 +97,9 @@ export default { } }, mounted() { + if (this.userIsAdmin) { + this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0) + } this.showFiles = this.expanded } } diff --git a/client/components/tables/TracksTable.vue b/client/components/tables/TracksTable.vue index 2554fff1b..5730ee364 100644 --- a/client/components/tables/TracksTable.vue +++ b/client/components/tables/TracksTable.vue @@ -6,7 +6,7 @@ <span class="text-sm font-mono">{{ tracks.length }}</span> </div> <div class="flex-grow" /> - <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $strings.ButtonFullPath }}</ui-btn> + <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn> <nuxt-link v-if="userCanUpdate && !isFile" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent> <ui-btn small color="primary">{{ $strings.ButtonManageTracks }}</ui-btn> </nuxt-link> @@ -74,6 +74,10 @@ export default { } }, methods: { + toggleFullPath() { + this.showFullPath = !this.showFullPath + localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0) + }, clickBar() { this.showTracks = !this.showTracks }, @@ -82,6 +86,10 @@ export default { this.showAudioFileDataModal = true } }, - mounted() {} + mounted() { + if (this.userIsAdmin) { + this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0) + } + } } </script> \ No newline at end of file From d6b17678ec99ed45a3bc52590ce3add2cca31e91 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 9 Nov 2023 16:36:28 -0600 Subject: [PATCH 143/208] Update:Persist soft/hard delete checkbox option #1689 --- client/components/app/Appbar.vue | 4 +++- client/components/cards/LazyBookCard.vue | 4 +++- client/pages/item/_id/index.vue | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index 92599c7aa..c281f8214 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -320,9 +320,11 @@ export default { checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox, yesButtonText: this.$strings.ButtonDelete, yesButtonColor: 'error', - checkboxDefaultValue: true, + checkboxDefaultValue: !Number(localStorage.getItem('softDeleteDefault') || 0), callback: (confirmed, hardDelete) => { if (confirmed) { + localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1) + this.$store.commit('setProcessingBatch', true) this.$axios diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 1b87df0f2..c4d1345da 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -848,9 +848,11 @@ export default { checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox, yesButtonText: this.$strings.ButtonDelete, yesButtonColor: 'error', - checkboxDefaultValue: true, + checkboxDefaultValue: !Number(localStorage.getItem('softDeleteDefault') || 0), callback: (confirmed, hardDelete) => { if (confirmed) { + localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1) + this.processing = true const axios = this.$axios || this.$nuxt.$axios axios diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 657d564d2..8658a6e42 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -686,9 +686,11 @@ export default { checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox, yesButtonText: this.$strings.ButtonDelete, yesButtonColor: 'error', - checkboxDefaultValue: true, + checkboxDefaultValue: !Number(localStorage.getItem('softDeleteDefault') || 0), callback: (confirmed, hardDelete) => { if (confirmed) { + localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1) + this.$axios .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`) .then(() => { From ea05e1f559efd2fd78851cd269bba07ad6ceaa48 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Fri, 10 Nov 2023 09:58:30 +0000 Subject: [PATCH 144/208] Remove test/ from .gitigore (now contains unit tests) --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6f47029b0..fbea7c5f5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ /podcasts/ /media/ /metadata/ -test/ /client/.nuxt/ /client/dist/ /dist/ From ecba67da6d1e8a8c47174c33ad62cc22e517174d Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Fri, 10 Nov 2023 10:02:02 +0000 Subject: [PATCH 145/208] Add Istanbul coverage (nyc) --- .gitignore | 2 + package-lock.json | 2721 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- 3 files changed, 2714 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index fbea7c5f5..9360600a5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ /client/dist/ /dist/ /deploy/ +/coverage/ +/.nyc_output/ sw.* .DS_STORE diff --git a/package-lock.json b/package-lock.json index c0f10bb8f..58ed36950 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,15 +28,557 @@ "chai": "^4.3.10", "mocha": "^10.2.0", "nodemon": "^2.0.20", + "nyc": "^15.1.0", "sinon": "^17.0.1" } }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/compat-data": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", + "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", + "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.3", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.3", + "@babel/types": "^7.23.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/parser": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -307,7 +849,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "optional": true, + "devOptional": true, "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -361,11 +903,29 @@ "node": ">= 8" } }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -378,6 +938,15 @@ "node": ">=10" } }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -478,6 +1047,38 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -515,6 +1116,21 @@ "node": ">= 10" } }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -527,6 +1143,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/chai": { "version": "4.3.10", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", @@ -633,11 +1278,36 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "optional": true, + "devOptional": true, "engines": { "node": ">=6" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -675,6 +1345,12 @@ "node": ">= 0.8" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -704,6 +1380,12 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -729,6 +1411,20 @@ "node": ">= 0.10" } }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -761,6 +1457,21 @@ "node": ">=6" } }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -869,6 +1580,12 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/electron-to-chromium": { + "version": "1.4.580", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.580.tgz", + "integrity": "sha512-T5q3pjQon853xxxHUq3ZP68ZpvJHuSMY2+BZaW3QzjS4HvNuvsMmZ/+lU+nCrftre1jFZ+OSlExynXWBihnXzw==", + "dev": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -986,6 +1703,12 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "optional": true }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1000,6 +1723,28 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1078,6 +1823,36 @@ "node": ">= 0.8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -1106,6 +1881,19 @@ } } }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -1135,6 +1923,26 @@ "node": ">= 0.6" } }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -1192,6 +2000,15 @@ "node": ">=10" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1223,6 +2040,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1254,6 +2080,15 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -1295,6 +2130,22 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1304,6 +2155,12 @@ "he": "bin/he" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/htmlparser2": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", @@ -1443,7 +2300,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "optional": true, + "devOptional": true, "engines": { "node": ">=0.8.19" } @@ -1452,7 +2309,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "optional": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -1564,6 +2421,24 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -1576,6 +2451,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -1586,7 +2470,239 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true + "devOptional": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } }, "node_modules/just-extend": { "version": "4.2.1", @@ -1594,11 +2710,29 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -2311,6 +3445,24 @@ "node": ">=10" } }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, "node_modules/node-tone": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz", @@ -2402,6 +3554,68 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2452,6 +3666,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -2467,6 +3708,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2492,6 +3757,15 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -2511,6 +3785,12 @@ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2523,6 +3803,30 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -2630,6 +3934,18 @@ "node": ">=8.10.0" } }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2639,6 +3955,21 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -2870,6 +4201,27 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -3102,6 +4454,38 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/sqlite3": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", @@ -3192,6 +4576,15 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3240,6 +4633,29 @@ "node": ">=8" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3291,6 +4707,15 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3303,6 +4728,15 @@ "node": ">= 0.6" } }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -3335,6 +4769,36 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3390,7 +4854,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "optional": true, + "devOptional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -3401,6 +4865,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -3445,6 +4915,18 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/ws": { "version": "8.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", @@ -3499,6 +4981,50 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs-parser/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/yargs-unparser": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", @@ -3526,6 +5052,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yargs/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -3540,12 +5081,438 @@ } }, "dependencies": { + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + } + } + }, + "@babel/compat-data": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", + "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "dev": true + }, + "@babel/core": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", + "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.3", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.3", + "@babel/types": "^7.23.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "dev": true, + "requires": { + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true + }, + "@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + } + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + } + } + }, + "@babel/parser": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "dev": true + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -3773,7 +5740,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "optional": true, + "devOptional": true, "requires": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -3809,11 +5776,26 @@ "picomatch": "^2.0.4" } }, + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, "aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -3823,6 +5805,15 @@ "readable-stream": "^3.6.0" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -3907,6 +5898,18 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3938,6 +5941,18 @@ "unique-filename": "^1.1.1" } }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -3947,6 +5962,18 @@ "get-intrinsic": "^1.0.2" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "dev": true + }, "chai": { "version": "4.3.10", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", @@ -4023,7 +6050,31 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "optional": true + "devOptional": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } }, "color-convert": { "version": "2.0.1", @@ -4053,6 +6104,12 @@ "delayed-stream": "~1.0.0" } }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4076,6 +6133,12 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -4095,6 +6158,17 @@ "vary": "^1" } }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -4118,6 +6192,15 @@ "type-detect": "^4.0.0" } }, + "default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4192,6 +6275,12 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "electron-to-chromium": { + "version": "1.4.580", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.580.tgz", + "integrity": "sha512-T5q3pjQon853xxxHUq3ZP68ZpvJHuSMY2+BZaW3QzjS4HvNuvsMmZ/+lU+nCrftre1jFZ+OSlExynXWBihnXzw==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4281,6 +6370,12 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "optional": true }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -4292,6 +6387,18 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -4358,6 +6465,27 @@ "unpipe": "~1.0.0" } }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, "flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -4369,6 +6497,16 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -4389,6 +6527,12 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -4430,6 +6574,12 @@ "wide-align": "^1.1.2" } }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4452,6 +6602,12 @@ "has-symbols": "^1.0.3" } }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4474,6 +6630,12 @@ "is-glob": "^4.0.1" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -4503,12 +6665,28 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "htmlparser2": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", @@ -4617,13 +6795,13 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "optional": true + "devOptional": true }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "optional": true + "devOptional": true }, "infer-owner": { "version": "1.0.4", @@ -4708,12 +6886,30 @@ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -4724,7 +6920,179 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true + "devOptional": true + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "requires": { + "append-transform": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "dependencies": { + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true }, "just-extend": { "version": "4.2.1", @@ -4732,11 +7100,26 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -5266,6 +7649,21 @@ } } }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, "node-tone": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz", @@ -5337,6 +7735,58 @@ "set-blocking": "^2.0.0" } }, + "nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + } + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5372,6 +7822,26 @@ "yocto-queue": "^0.1.0" } }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, "p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -5381,6 +7851,24 @@ "aggregate-error": "^3.0.0" } }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5397,6 +7885,12 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -5413,12 +7907,36 @@ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -5502,12 +8020,33 @@ "picomatch": "^2.2.1" } }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -5656,6 +8195,21 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -5835,6 +8389,32 @@ } } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "sqlite3": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", @@ -5901,6 +8481,12 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5936,6 +8522,23 @@ } } }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5975,6 +8578,12 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -5984,6 +8593,15 @@ "mime-types": "~2.1.24" } }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -6013,6 +8631,16 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6056,11 +8684,17 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "optional": true, + "devOptional": true, "requires": { "isexe": "^2.0.0" } }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -6099,6 +8733,18 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "ws": { "version": "8.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", @@ -6130,6 +8776,57 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + } + } + }, "yargs-unparser": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", diff --git a/package.json b/package.json index 62e1eb2e1..32b3840b8 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "docker-arm64-local": "docker buildx build --platform linux/arm64 --load . -t advplyr/audiobookshelf-arm64-local", "docker-armv7-local": "docker buildx build --platform linux/arm/v7 --load . -t advplyr/audiobookshelf-armv7-local", "deploy-linux": "node deploy/linux", - "test": "mocha" + "test": "mocha", + "coverage": "nyc mocha" }, "bin": "prod.js", "pkg": { @@ -51,6 +52,7 @@ "chai": "^4.3.10", "mocha": "^10.2.0", "nodemon": "^2.0.20", + "nyc": "^15.1.0", "sinon": "^17.0.1" } } From 237fe84c54341b788c99a099b1f6018698074c2f Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 10 Nov 2023 16:11:51 -0600 Subject: [PATCH 146/208] Add new API endpoint for updating auth-settings and update passport auth strategies --- client/pages/config/authentication.vue | 20 ++- server/Auth.js | 196 +++++++++++++--------- server/controllers/MiscController.js | 88 +++++++++- server/objects/settings/ServerSettings.js | 68 ++++---- server/routers/ApiRouter.js | 2 + 5 files changed, 255 insertions(+), 119 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 0da486c11..9ea8172ab 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -199,13 +199,19 @@ export default { if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid') this.savingSettings = true - const success = await this.$store.dispatch('updateServerSettings', this.newAuthSettings) - this.savingSettings = false - if (success) { - this.$toast.success('Server settings updated') - } else { - this.$toast.error('Failed to update server settings') - } + this.$axios + .$patch('/api/auth-settings', this.newAuthSettings) + .then((data) => { + this.$store.commit('setServerSettings', data.serverSettings) + this.$toast.success('Server settings updated') + }) + .catch((error) => { + console.error('Failed to update server settings', error) + this.$toast.error('Failed to update server settings') + }) + .finally(() => { + this.savingSettings = false + }) }, init() { this.newAuthSettings = { diff --git a/server/Auth.js b/server/Auth.js index eeb7ad472..15e365768 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -36,7 +36,12 @@ class Auth { async initPassportJs() { // Check if we should load the local strategy (username + password login) if (global.ServerSettings.authActiveAuthMethods.includes("local")) { - passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) + this.initAuthStrategyPassword() + } + + // Check if we should load the openid strategy + if (global.ServerSettings.authActiveAuthMethods.includes("openid")) { + this.initAuthStrategyOpenID() } // Check if we should load the google-oauth20 strategy @@ -62,84 +67,6 @@ class Auth { }).bind(this))) } - // Check if we should load the openid strategy - if (global.ServerSettings.authActiveAuthMethods.includes("openid")) { - const openIdIssuerClient = new OpenIDClient.Issuer({ - issuer: global.ServerSettings.authOpenIDIssuerURL, - authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL, - token_endpoint: global.ServerSettings.authOpenIDTokenURL, - userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL, - jwks_uri: global.ServerSettings.authOpenIDJwksURL - }).Client - const openIdClient = new openIdIssuerClient({ - client_id: global.ServerSettings.authOpenIDClientID, - client_secret: global.ServerSettings.authOpenIDClientSecret - }) - passport.use('openid-client', new OpenIDClient.Strategy({ - client: openIdClient, - params: { - redirect_uri: '/auth/openid/callback', - scope: 'openid profile email' - } - }, async (tokenset, userinfo, done) => { - Logger.debug(`[Auth] openid callback userinfo=`, userinfo) - - if (!userinfo.sub) { - Logger.error(`[Auth] openid callback invalid userinfo, no sub`) - return done(null, null) - } - - // First check for matching user by sub - let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) - if (!user) { - // Optionally match existing by email or username based on server setting "authOpenIDMatchExistingBy" - if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) { - Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`) - user = await Database.userModel.getUserByEmail(userinfo.email) - // Check that user is not already matched - if (user?.authOpenIDSub) { - Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) - // TODO: Show some error log? - user = null - } - } else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) { - Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`) - user = await Database.userModel.getUserByUsername(userinfo.preferred_username) - // Check that user is not already matched - if (user?.authOpenIDSub) { - Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) - // TODO: Show some error log? - user = null - } - } - - // If existing user was matched and isActive then save sub to user - if (user?.isActive) { - Logger.info(`[Auth] openid: New user found matching existing user "${user.username}"`) - user.authOpenIDSub = userinfo.sub - await Database.userModel.updateFromOld(user) - } else if (user && !user.isActive) { - Logger.warn(`[Auth] openid: New user found matching existing user "${user.username}" but that user is deactivated`) - } - - // Optionally auto register the user - if (!user && Database.serverSettings.authOpenIDAutoRegister) { - Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo) - user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this) - } - } - - if (!user?.isActive) { - // deny login - done(null, null) - return - } - - // permit login - return done(null, user) - })) - } - // Load the JwtStrategy (always) -> for bearer token auth passport.use(new JwtStrategy({ jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]), @@ -167,6 +94,117 @@ class Auth { }).bind(this)) } + /** + * Passport use LocalStrategy + */ + initAuthStrategyPassword() { + passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) + } + + /** + * Passport use OpenIDClient.Strategy + */ + initAuthStrategyOpenID() { + const openIdIssuerClient = new OpenIDClient.Issuer({ + issuer: global.ServerSettings.authOpenIDIssuerURL, + authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL, + token_endpoint: global.ServerSettings.authOpenIDTokenURL, + userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL, + jwks_uri: global.ServerSettings.authOpenIDJwksURL + }).Client + const openIdClient = new openIdIssuerClient({ + client_id: global.ServerSettings.authOpenIDClientID, + client_secret: global.ServerSettings.authOpenIDClientSecret + }) + passport.use('openid-client', new OpenIDClient.Strategy({ + client: openIdClient, + params: { + redirect_uri: '/auth/openid/callback', + scope: 'openid profile email' + } + }, async (tokenset, userinfo, done) => { + Logger.debug(`[Auth] openid callback userinfo=`, userinfo) + + if (!userinfo.sub) { + Logger.error(`[Auth] openid callback invalid userinfo, no sub`) + return done(null, null) + } + + // First check for matching user by sub + let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) + if (!user) { + // Optionally match existing by email or username based on server setting "authOpenIDMatchExistingBy" + if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) { + Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`) + user = await Database.userModel.getUserByEmail(userinfo.email) + // Check that user is not already matched + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) + // TODO: Show some error log? + user = null + } + } else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) { + Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`) + user = await Database.userModel.getUserByUsername(userinfo.preferred_username) + // Check that user is not already matched + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) + // TODO: Show some error log? + user = null + } + } + + // If existing user was matched and isActive then save sub to user + if (user?.isActive) { + Logger.info(`[Auth] openid: New user found matching existing user "${user.username}"`) + user.authOpenIDSub = userinfo.sub + await Database.userModel.updateFromOld(user) + } else if (user && !user.isActive) { + Logger.warn(`[Auth] openid: New user found matching existing user "${user.username}" but that user is deactivated`) + } + + // Optionally auto register the user + if (!user && Database.serverSettings.authOpenIDAutoRegister) { + Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo) + user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this) + } + } + + if (!user?.isActive) { + // deny login + done(null, null) + return + } + + // permit login + return done(null, user) + })) + } + + /** + * Unuse strategy + * + * @param {string} name + */ + unuseAuthStrategy(name) { + passport.unuse(name) + } + + /** + * Use strategy + * + * @param {string} name + */ + useAuthStrategy(name) { + if (name === 'openid') { + this.initAuthStrategyOpenID() + } else if (name === 'local') { + this.initAuthStrategyPassword() + } else { + Logger.error('[Auth] Invalid auth strategy ' + name) + } + } + /** * Stores the client's choice how the login callback should happen in temp cookies * diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 9b58188f5..6d7507cf1 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -129,7 +129,7 @@ class MiscController { return res.sendStatus(403) } const settingsUpdate = req.body - if (!settingsUpdate || !isObject(settingsUpdate)) { + if (!isObject(settingsUpdate)) { return res.status(400).send('Invalid settings update object') } @@ -604,5 +604,91 @@ class MiscController { } return res.json(Database.serverSettings.authenticationSettings) } + + /** + * PATCH: api/auth-settings + * @this import('../routers/ApiRouter') + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async updateAuthSettings(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to update auth settings`) + return res.sendStatus(403) + } + + const settingsUpdate = req.body + if (!isObject(settingsUpdate)) { + return res.status(400).send('Invalid auth settings update object') + } + + let hasUpdates = false + + const currentAuthenticationSettings = Database.serverSettings.authenticationSettings + const originalAuthMethods = [...currentAuthenticationSettings.authActiveAuthMethods] + + // TODO: Better validation of auth settings once auth settings are separated from server settings + for (const key in currentAuthenticationSettings) { + if (settingsUpdate[key] === undefined) continue + + if (key === 'authActiveAuthMethods') { + let updatedAuthMethods = settingsUpdate[key]?.filter?.((authMeth) => Database.serverSettings.supportedAuthMethods.includes(authMeth)) + if (Array.isArray(updatedAuthMethods) && updatedAuthMethods.length) { + updatedAuthMethods.sort() + currentAuthenticationSettings[key].sort() + if (updatedAuthMethods.join() !== currentAuthenticationSettings[key].join()) { + Logger.debug(`[MiscController] Updating auth settings key "authActiveAuthMethods" from "${currentAuthenticationSettings[key].join()}" to "${updatedAuthMethods.join()}"`) + Database.serverSettings[key] = updatedAuthMethods + hasUpdates = true + } + } else { + Logger.warn(`[MiscController] Invalid value for authActiveAuthMethods`) + } + } else { + const updatedValueType = typeof settingsUpdate[key] + if (['authOpenIDAutoLaunch', 'authOpenIDAutoRegister'].includes(key)) { + if (updatedValueType !== 'boolean') { + Logger.warn(`[MiscController] Invalid value for ${key}. Expected boolean`) + continue + } + } else if (updatedValueType !== null && updatedValueType !== 'string') { + Logger.warn(`[MiscController] Invalid value for ${key}. Expected string or null`) + continue + } + let updatedValue = settingsUpdate[key] + if (updatedValue === '') updatedValue = null + let currentValue = currentAuthenticationSettings[key] + if (currentValue === '') currentValue = null + + if (updatedValue !== currentValue) { + Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${currentValue}" to "${updatedValue}"`) + Database.serverSettings[key] = updatedValue + hasUpdates = true + } + } + } + + if (hasUpdates) { + // Use/unuse auth methods + Database.serverSettings.supportedAuthMethods.forEach((authMethod) => { + if (originalAuthMethods.includes(authMethod) && !Database.serverSettings.authActiveAuthMethods.includes(authMethod)) { + // Auth method has been removed + Logger.info(`[MiscController] Disabling active auth method "${authMethod}"`) + this.auth.unuseAuthStrategy(authMethod) + } else if (!originalAuthMethods.includes(authMethod) && Database.serverSettings.authActiveAuthMethods.includes(authMethod)) { + // Auth method has been added + Logger.info(`[MiscController] Enabling active auth method "${authMethod}"`) + this.auth.useAuthStrategy(authMethod) + } + }) + + await Database.updateServerSettings() + } + + res.json({ + serverSettings: Database.serverSettings.toJSONForBrowser() + }) + } } module.exports = new MiscController() \ No newline at end of file diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 05a64d065..afde1ddf4 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -59,19 +59,19 @@ class ServerSettings { this.authActiveAuthMethods = ['local'] // google-oauth20 settings - this.authGoogleOauth20ClientID = '' - this.authGoogleOauth20ClientSecret = '' - this.authGoogleOauth20CallbackURL = '' + this.authGoogleOauth20ClientID = null + this.authGoogleOauth20ClientSecret = null + this.authGoogleOauth20CallbackURL = null // openid settings - this.authOpenIDIssuerURL = '' - this.authOpenIDAuthorizationURL = '' - this.authOpenIDTokenURL = '' - this.authOpenIDUserInfoURL = '' - this.authOpenIDJwksURL = '' - this.authOpenIDLogoutURL = '' - this.authOpenIDClientID = '' - this.authOpenIDClientSecret = '' + this.authOpenIDIssuerURL = null + this.authOpenIDAuthorizationURL = null + this.authOpenIDTokenURL = null + this.authOpenIDUserInfoURL = null + this.authOpenIDJwksURL = null + this.authOpenIDLogoutURL = null + this.authOpenIDClientID = null + this.authOpenIDClientSecret = null this.authOpenIDButtonText = 'Login with OpenId' this.authOpenIDAutoLaunch = false this.authOpenIDAutoRegister = false @@ -118,18 +118,18 @@ class ServerSettings { this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] - this.authGoogleOauth20ClientID = settings.authGoogleOauth20ClientID || '' - this.authGoogleOauth20ClientSecret = settings.authGoogleOauth20ClientSecret || '' - this.authGoogleOauth20CallbackURL = settings.authGoogleOauth20CallbackURL || '' + this.authGoogleOauth20ClientID = settings.authGoogleOauth20ClientID || null + this.authGoogleOauth20ClientSecret = settings.authGoogleOauth20ClientSecret || null + this.authGoogleOauth20CallbackURL = settings.authGoogleOauth20CallbackURL || null - this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || '' - this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || '' - this.authOpenIDTokenURL = settings.authOpenIDTokenURL || '' - this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || '' - this.authOpenIDJwksURL = settings.authOpenIDJwksURL || '' - this.authOpenIDLogoutURL = settings.authOpenIDLogoutURL || '' - this.authOpenIDClientID = settings.authOpenIDClientID || '' - this.authOpenIDClientSecret = settings.authOpenIDClientSecret || '' + this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null + this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || null + this.authOpenIDTokenURL = settings.authOpenIDTokenURL || null + this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || null + this.authOpenIDJwksURL = settings.authOpenIDJwksURL || null + this.authOpenIDLogoutURL = settings.authOpenIDLogoutURL || null + this.authOpenIDClientID = settings.authOpenIDClientID || null + this.authOpenIDClientSecret = settings.authOpenIDClientSecret || null this.authOpenIDButtonText = settings.authOpenIDButtonText || 'Login with OpenId' this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister @@ -142,9 +142,9 @@ class ServerSettings { // remove uninitialized methods // GoogleOauth20 if (this.authActiveAuthMethods.includes('google-oauth20') && ( - this.authGoogleOauth20ClientID === '' || - this.authGoogleOauth20ClientSecret === '' || - this.authGoogleOauth20CallbackURL === '' + !this.authGoogleOauth20ClientID || + !this.authGoogleOauth20ClientSecret || + !this.authGoogleOauth20CallbackURL )) { this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1) } @@ -152,13 +152,13 @@ class ServerSettings { // remove uninitialized methods // OpenID if (this.authActiveAuthMethods.includes('openid') && ( - this.authOpenIDIssuerURL === '' || - this.authOpenIDAuthorizationURL === '' || - this.authOpenIDTokenURL === '' || - this.authOpenIDUserInfoURL === '' || - this.authOpenIDJwksURL === '' || - this.authOpenIDClientID === '' || - this.authOpenIDClientSecret === '' + !this.authOpenIDIssuerURL || + !this.authOpenIDAuthorizationURL || + !this.authOpenIDTokenURL || + !this.authOpenIDUserInfoURL || + !this.authOpenIDJwksURL || + !this.authOpenIDClientID || + !this.authOpenIDClientSecret )) { this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('openid', 0), 1) } @@ -254,6 +254,10 @@ class ServerSettings { return json } + get supportedAuthMethods() { + return ['local', 'openid'] + } + get authenticationSettings() { return { authActiveAuthMethods: this.authActiveAuthMethods, diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index c4c0df5ee..8c97d59bb 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -35,6 +35,7 @@ const Series = require('../objects/entities/Series') class ApiRouter { constructor(Server) { + /** @type {import('../Auth')} */ this.auth = Server.auth this.playbackSessionManager = Server.playbackSessionManager this.abMergeManager = Server.abMergeManager @@ -310,6 +311,7 @@ class ApiRouter { this.router.delete('/genres/:genre', MiscController.deleteGenre.bind(this)) this.router.post('/validate-cron', MiscController.validateCronExpression.bind(this)) this.router.get('/auth-settings', MiscController.getAuthSettings.bind(this)) + this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this)) this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this)) } From cff2caa07aced05165fa130a3b7d95be5e081d40 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 10 Nov 2023 16:32:14 -0600 Subject: [PATCH 147/208] Update:Rename podcast search page to add #2301 --- client/components/app/BookShelfToolbar.vue | 2 +- client/components/app/SideRail.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 9b79bc0f0..ab33a5b36 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -36,7 +36,7 @@ </svg> </nuxt-link> <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> - <p class="text-sm">{{ $strings.ButtonSearch }}</p> + <p class="text-sm">{{ $strings.ButtonAdd }}</p> </nuxt-link> </div> <div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8"> diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index deb96a6cb..56207526a 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -82,7 +82,7 @@ <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <span class="abs-icons icon-podcast text-xl"></span> - <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSearch }}</p> + <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAdd }}</p> <div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> </nuxt-link> From 557ef2ef798daf5d18b9206de586087b7f389de2 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 11 Nov 2023 10:52:05 -0600 Subject: [PATCH 148/208] Update /auth/openid endpoints for correct PKCE handling - Provide error handling for /auth/openid - Add session.mobile inside /auth/openid - Proper PKCE handling for /auth/openid/callback - redirect_uri handling for the token url in /auth/openid/callback Co-authored-by: Denis Arnst <git@sapd.eu> --- server/Auth.js | 154 +++++++++++++++++++++++++++++-------------------- 1 file changed, 90 insertions(+), 64 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 15e365768..54fa52a41 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -306,82 +306,108 @@ class Auth { // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { - // helper function from openid-client - function pick(object, ...paths) { - const obj = {} - for (const path of paths) { - if (object[path] !== undefined) { - obj[path] = object[path] + try { + // helper function from openid-client + function pick(object, ...paths) { + const obj = {} + for (const path of paths) { + if (object[path] !== undefined) { + obj[path] = object[path] + } } + return obj } - return obj - } - // Get the OIDC client from the strategy - // We need to call the client manually, because the strategy does not support forwarding the code challenge - // for API or mobile clients - const oidcStrategy = passport._strategy('openid-client') - oidcStrategy._params.redirect_uri = new URL(`${req.protocol}://${req.get('host')}/auth/openid/callback`).toString() - const client = oidcStrategy._client - const sessionKey = oidcStrategy._key + // Get the OIDC client from the strategy + // We need to call the client manually, because the strategy does not support forwarding the code challenge + // for API or mobile clients + const oidcStrategy = passport._strategy('openid-client') + oidcStrategy._params.redirect_uri = new URL(`${req.protocol}://${req.get('host')}/auth/openid/callback`).toString() + const client = oidcStrategy._client + const sessionKey = oidcStrategy._key - let code_challenge - let code_challenge_method + let code_challenge + let code_challenge_method - // If code_challenge is provided, expect that code_verifier will be handled by the client (mobile app) - // The web frontend of ABS does not need to do a PKCE itself, because it never handles the "code" of the oauth flow - // and as such will not send a code challenge, we will generate then one - if (req.query.code_challenge) { - code_challenge = req.query.code_challenge - code_challenge_method = req.query.code_challenge_method || 'S256' + // If code_challenge is provided, expect that code_verifier will be handled by the client (mobile app) + // The web frontend of ABS does not need to do a PKCE itself, because it never handles the "code" of the oauth flow + // and as such will not send a code challenge, we will generate then one + if (req.query.code_challenge) { + code_challenge = req.query.code_challenge + code_challenge_method = req.query.code_challenge_method || 'S256' - if (!['S256', 'plain'].includes(code_challenge_method)) { - return res.status(400).send('Invalid code_challenge_method') + if (!['S256', 'plain'].includes(code_challenge_method)) { + return res.status(400).send('Invalid code_challenge_method') + } + } else { + // If no code_challenge is provided, assume a web application flow and generate one + const code_verifier = OpenIDClient.generators.codeVerifier() + code_challenge = OpenIDClient.generators.codeChallenge(code_verifier) + code_challenge_method = 'S256' + + // Store the code_verifier in the session for later use in the token exchange + req.session[sessionKey] = { ...req.session[sessionKey], code_verifier } } - } else { - // If no code_challenge is provided, assume a web application flow and generate one - const code_verifier = OpenIDClient.generators.codeVerifier() - code_challenge = OpenIDClient.generators.codeChallenge(code_verifier) - code_challenge_method = 'S256' - // Store the code_verifier in the session for later use in the token exchange - req.session[sessionKey] = { ...req.session[sessionKey], code_verifier } + const params = { + state: OpenIDClient.generators.random(), + // Other params by the passport strategy + ...oidcStrategy._params + } + + if (!params.nonce && params.response_type.includes('id_token')) { + params.nonce = OpenIDClient.generators.random() + } + + req.session[sessionKey] = { + ...req.session[sessionKey], + ...pick(params, 'nonce', 'state', 'max_age', 'response_type') + } + + // Now get the URL to direct to + const authorizationUrl = client.authorizationUrl({ + ...params, + scope: 'openid profile email', + response_type: 'code', + code_challenge, + code_challenge_method, + }) + + // params (isRest, callback) to a cookie that will be send to the client + this.paramsToCookies(req, res) + + // Redirect the user agent (browser) to the authorization URL + res.redirect(authorizationUrl) + } catch (error) { + Logger.error(`[Auth] Error in /auth/openid route: ${error}`) + res.status(500).send('Internal Server Error') } - - const params = { - state: OpenIDClient.generators.random(), - // Other params by the passport strategy - ...oidcStrategy._params - } - - if (!params.nonce && params.response_type.includes('id_token')) { - params.nonce = OpenIDClient.generators.random() - } - - req.session[sessionKey] = { - ...req.session[sessionKey], - ...pick(params, 'nonce', 'state', 'max_age', 'response_type') - } - - // Now get the URL to direct to - const authorizationUrl = client.authorizationUrl({ - ...params, - scope: 'openid profile email', - response_type: 'code', - code_challenge, - code_challenge_method, - }) - - // params (isRest, callback) to a cookie that will be send to the client - this.paramsToCookies(req, res) - - // Redirect the user agent (browser) to the authorization URL - res.redirect(authorizationUrl) }) // openid strategy callback route (this receives the token from the configured openid login provider) - router.get('/auth/openid/callback', - passport.authenticate('openid-client'), + router.get('/auth/openid/callback', (req, res, next) => { + const oidcStrategy = passport._strategy('openid-client') + const sessionKey = oidcStrategy._key + + if (!req.session[sessionKey]) { + return res.status(400).send('No session') + } + + // If the client sends us a code_verifier, we will tell passport to use this to send this in the token request + // The code_verifier will be validated by the oauth2 provider by comparing it to the code_challenge in the first request + // Crucial for API/Mobile clients + if (req.query.code_verifier) { + req.session[sessionKey].code_verifier = req.query.code_verifier + } + + // While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request + // We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided + if (req.session[sessionKey].mobile) { + return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' })(req, res, next) + } else { + return passport.authenticate('openid-client')(req, res, next) + } + }, // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this)) From 1ad6722e6d5fb73fa754d64fc3be7e9f9205fcce Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 11 Nov 2023 11:29:59 -0600 Subject: [PATCH 149/208] Remove google-oauth passport strategy --- client/assets/app.css | 20 +++++++++ client/components/ui/Btn.vue | 24 +---------- client/pages/login.vue | 19 ++------ package-lock.json | 84 ------------------------------------ package.json | 1 - server/Auth.js | 39 ----------------- 6 files changed, 25 insertions(+), 162 deletions(-) diff --git a/client/assets/app.css b/client/assets/app.css index b7b8499d2..1a83dc1c1 100644 --- a/client/assets/app.css +++ b/client/assets/app.css @@ -258,4 +258,24 @@ Bookshelf Label .no-bars .Vue-Toastification__container.top-right { padding-top: 8px; +} + +.abs-btn::before { + content: ''; + position: absolute; + border-radius: 6px; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0); + transition: all 0.1s ease-in-out; +} + +.abs-btn:hover:not(:disabled)::before { + background-color: rgba(255, 255, 255, 0.1); +} + +.abs-btn:disabled::before { + background-color: rgba(0, 0, 0, 0.2); } \ No newline at end of file diff --git a/client/components/ui/Btn.vue b/client/components/ui/Btn.vue index d9b757154..7f73a956e 100644 --- a/client/components/ui/Btn.vue +++ b/client/components/ui/Btn.vue @@ -1,5 +1,5 @@ <template> - <nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList"> + <nuxt-link v-if="to" :to="to" class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList"> <slot /> <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> <svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24"> @@ -7,7 +7,7 @@ </svg> </div> </nuxt-link> - <button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click"> + <button v-else class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click"> <slot /> <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> <svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24"> @@ -72,23 +72,3 @@ export default { mounted() {} } </script> - -<style scoped> -.btn::before { - content: ''; - position: absolute; - border-radius: 6px; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(255, 255, 255, 0); - transition: all 0.1s ease-in-out; -} -.btn:hover:not(:disabled)::before { - background-color: rgba(255, 255, 255, 0.1); -} -button:disabled::before { - background-color: rgba(0, 0, 0, 0.2); -} -</style> \ No newline at end of file diff --git a/client/pages/login.vue b/client/pages/login.vue index 724f5999f..6ec67b00a 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -41,14 +41,11 @@ </div> </form> - <div v-if="login_local && (login_google_oauth20 || login_openid)" class="w-full h-px bg-white bg-opacity-10 my-4" /> + <div v-if="login_local && login_openid" class="w-full h-px bg-white bg-opacity-10 my-4" /> <div class="w-full flex py-3"> - <a v-show="login_google_oauth20" :href="googleAuthUri"> - <ui-btn color="primary" class="leading-none">Login with Google</ui-btn> - </a> - <a v-show="login_openid" :href="openidAuthUri"> - <ui-btn color="primary" class="leading-none">{{ openIDButtonText }}</ui-btn> + <a v-if="login_openid" :href="openidAuthUri" class="w-full abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center bg-primary text-white px-8 py-2 leading-none"> + {{ openIDButtonText }} </a> </div> </div> @@ -76,7 +73,6 @@ export default { ConfigPath: '', MetadataPath: '', login_local: true, - login_google_oauth20: false, login_openid: false, authFormData: null } @@ -112,9 +108,6 @@ export default { user() { return this.$store.state.user.user }, - googleAuthUri() { - return `${process.env.serverUrl}/auth/google?callback=${location.toString()}` - }, openidAuthUri() { return `${process.env.serverUrl}/auth/openid?callback=${location.toString()}` }, @@ -251,12 +244,6 @@ export default { this.login_local = false } - if (authMethods.includes('google-oauth20')) { - this.login_google_oauth20 = true - } else { - this.login_google_oauth20 = false - } - if (authMethods.includes('openid')) { // Auto redirect unless query string ?autoLaunch=0 if (this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') { diff --git a/package-lock.json b/package-lock.json index dd2b8339b..b068fe8e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,6 @@ "nodemailer": "^6.9.2", "openid-client": "^5.6.1", "passport": "^0.6.0", - "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "sequelize": "^6.32.1", "socket.io": "^4.5.4", @@ -320,14 +319,6 @@ "node": "^4.5.0 || >= 5.9" } }, - "node_modules/base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1878,11 +1869,6 @@ "set-blocking": "^2.0.0" } }, - "node_modules/oauth": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", - "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1996,17 +1982,6 @@ "url": "https://github.com/sponsors/jaredhanson" } }, - "node_modules/passport-google-oauth20": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", - "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", - "dependencies": { - "passport-oauth2": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/passport-jwt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", @@ -2016,25 +1991,6 @@ "passport-strategy": "^1.0.0" } }, - "node_modules/passport-oauth2": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", - "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", - "dependencies": { - "base64url": "3.x.x", - "oauth": "0.9.x", - "passport-strategy": "1.x.x", - "uid2": "0.0.x", - "utils-merge": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -2772,11 +2728,6 @@ "node": ">= 0.8" } }, - "node_modules/uid2": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", - "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" - }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -3175,11 +3126,6 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, - "base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -4347,11 +4293,6 @@ "set-blocking": "^2.0.0" } }, - "oauth": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", - "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4428,14 +4369,6 @@ "utils-merge": "^1.0.1" } }, - "passport-google-oauth20": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", - "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", - "requires": { - "passport-oauth2": "1.x.x" - } - }, "passport-jwt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", @@ -4445,18 +4378,6 @@ "passport-strategy": "^1.0.0" } }, - "passport-oauth2": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", - "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", - "requires": { - "base64url": "3.x.x", - "oauth": "0.9.x", - "passport-strategy": "1.x.x", - "uid2": "0.0.x", - "utils-merge": "1.x.x" - } - }, "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -4984,11 +4905,6 @@ "random-bytes": "~1.0.0" } }, - "uid2": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", - "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" - }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/package.json b/package.json index d4e9c209f..6e283cdc0 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "nodemailer": "^6.9.2", "openid-client": "^5.6.1", "passport": "^0.6.0", - "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "sequelize": "^6.32.1", "socket.io": "^4.5.4", diff --git a/server/Auth.js b/server/Auth.js index 54fa52a41..f504e315d 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -5,7 +5,6 @@ const jwt = require('./libs/jsonwebtoken') const LocalStrategy = require('./libs/passportLocal') const JwtStrategy = require('passport-jwt').Strategy const ExtractJwt = require('passport-jwt').ExtractJwt -const GoogleStrategy = require('passport-google-oauth20').Strategy const OpenIDClient = require('openid-client') const Database = require('./Database') const Logger = require('./Logger') @@ -44,29 +43,6 @@ class Auth { this.initAuthStrategyOpenID() } - // Check if we should load the google-oauth20 strategy - if (global.ServerSettings.authActiveAuthMethods.includes("google-oauth20")) { - passport.use(new GoogleStrategy({ - clientID: global.ServerSettings.authGoogleOauth20ClientID, - clientSecret: global.ServerSettings.authGoogleOauth20ClientSecret, - callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL - }, (async function (accessToken, refreshToken, profile, done) { - // TODO: do we want to create the users which does not exist? - - // get user by email - const user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) - - if (!user || !user.isActive) { - // deny login - done(null, null) - return - } - - // permit login - return done(null, user) - }).bind(this))) - } - // Load the JwtStrategy (always) -> for bearer token auth passport.use(new JwtStrategy({ jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]), @@ -289,21 +265,6 @@ class Auth { res.json(await this.getUserLoginResponsePayload(req.user)) }) - // google-oauth20 strategy login route (this redirects to the google login) - router.get('/auth/google', (req, res, next) => { - const auth_func = passport.authenticate('google', { scope: ['email'] }) - // params (isRest, callback) to a cookie that will be send to the client - this.paramsToCookies(req, res) - auth_func(req, res, next) - }) - - // google-oauth20 strategy callback route (this receives the token from google) - router.get('/auth/google/callback', - passport.authenticate('google'), - // on a successfull login: read the cookies and react like the client requested (callback or json) - this.handleLoginSuccessBasedOnCookie.bind(this) - ) - // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { try { From fb48636510ab873ba3ae0cbd3be2492855c99d0d Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 11 Nov 2023 13:10:24 -0600 Subject: [PATCH 150/208] Openid auth failures redirect to login page with error message. Remove remaining google oauth server settings --- client/pages/login.vue | 15 ++++++++++--- server/Auth.js | 16 +++++++++----- server/controllers/MiscController.js | 2 +- server/objects/settings/ServerSettings.js | 26 ----------------------- 4 files changed, 24 insertions(+), 35 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 6ec67b00a..f7579dd61 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -109,7 +109,7 @@ export default { return this.$store.state.user.user }, openidAuthUri() { - return `${process.env.serverUrl}/auth/openid?callback=${location.toString()}` + return `${process.env.serverUrl}/auth/openid?callback=${location.href.split('?').shift()}` }, openIDButtonText() { return this.authFormData?.authOpenIDButtonText || 'Login with OpenId' @@ -238,6 +238,15 @@ export default { }) }, updateLoginVisibility(authMethods) { + if (this.$route.query?.error) { + this.error = this.$route.query.error + + // Remove error query string + const newurl = new URL(location.href) + newurl.searchParams.delete('error') + window.history.replaceState({ path: newurl.href }, '', newurl.href) + } + if (authMethods.includes('local') || !authMethods.length) { this.login_local = true } else { @@ -257,8 +266,8 @@ export default { } }, async mounted() { - if (new URLSearchParams(window.location.search).get('setToken')) { - localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) + if (this.$route.query?.setToken) { + localStorage.setItem('token', this.$route.query.setToken) } if (localStorage.getItem('token')) { if (await this.checkAuth()) return // if valid user no need to check status diff --git a/server/Auth.js b/server/Auth.js index f504e315d..06db47a8d 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -101,9 +101,10 @@ class Auth { }, async (tokenset, userinfo, done) => { Logger.debug(`[Auth] openid callback userinfo=`, userinfo) + let failureMessage = 'Unauthorized' if (!userinfo.sub) { Logger.error(`[Auth] openid callback invalid userinfo, no sub`) - return done(null, null) + return done(null, null, failureMessage) } // First check for matching user by sub @@ -116,7 +117,8 @@ class Auth { // Check that user is not already matched if (user?.authOpenIDSub) { Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) - // TODO: Show some error log? + // TODO: Message isn't actually returned to the user yet. Need to override the passport authenticated callback + failureMessage = 'A matching user was found but is already matched with another user from your auth provider' user = null } } else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) { @@ -125,7 +127,8 @@ class Auth { // Check that user is not already matched if (user?.authOpenIDSub) { Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) - // TODO: Show some error log? + // TODO: Message isn't actually returned to the user yet. Need to override the passport authenticated callback + failureMessage = 'A matching user was found but is already matched with another user from your auth provider' user = null } } @@ -147,8 +150,11 @@ class Auth { } if (!user?.isActive) { + if (user && !user.isActive) { + failureMessage = 'Unauthorized' + } // deny login - done(null, null) + done(null, null, failureMessage) return } @@ -366,7 +372,7 @@ class Auth { if (req.session[sessionKey].mobile) { return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' })(req, res, next) } else { - return passport.authenticate('openid-client')(req, res, next) + return passport.authenticate('openid-client', { failureRedirect: '/login?error=Unauthorized&autoLaunch=0' })(req, res, next) } }, // on a successfull login: read the cookies and react like the client requested (callback or json) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 6d7507cf1..11adf3e92 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -652,7 +652,7 @@ class MiscController { Logger.warn(`[MiscController] Invalid value for ${key}. Expected boolean`) continue } - } else if (updatedValueType !== null && updatedValueType !== 'string') { + } else if (settingsUpdate[key] !== null && updatedValueType !== 'string') { Logger.warn(`[MiscController] Invalid value for ${key}. Expected string or null`) continue } diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index afde1ddf4..df5e71f13 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -58,11 +58,6 @@ class ServerSettings { // Active auth methodes this.authActiveAuthMethods = ['local'] - // google-oauth20 settings - this.authGoogleOauth20ClientID = null - this.authGoogleOauth20ClientSecret = null - this.authGoogleOauth20CallbackURL = null - // openid settings this.authOpenIDIssuerURL = null this.authOpenIDAuthorizationURL = null @@ -118,9 +113,6 @@ class ServerSettings { this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] - this.authGoogleOauth20ClientID = settings.authGoogleOauth20ClientID || null - this.authGoogleOauth20ClientSecret = settings.authGoogleOauth20ClientSecret || null - this.authGoogleOauth20CallbackURL = settings.authGoogleOauth20CallbackURL || null this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || null @@ -139,16 +131,6 @@ class ServerSettings { this.authActiveAuthMethods = ['local'] } - // remove uninitialized methods - // GoogleOauth20 - if (this.authActiveAuthMethods.includes('google-oauth20') && ( - !this.authGoogleOauth20ClientID || - !this.authGoogleOauth20ClientSecret || - !this.authGoogleOauth20CallbackURL - )) { - this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1) - } - // remove uninitialized methods // OpenID if (this.authActiveAuthMethods.includes('openid') && ( @@ -226,9 +208,6 @@ class ServerSettings { version: this.version, buildNumber: this.buildNumber, authActiveAuthMethods: this.authActiveAuthMethods, - authGoogleOauth20ClientID: this.authGoogleOauth20ClientID, // Do not return to client - authGoogleOauth20ClientSecret: this.authGoogleOauth20ClientSecret, // Do not return to client - authGoogleOauth20CallbackURL: this.authGoogleOauth20CallbackURL, authOpenIDIssuerURL: this.authOpenIDIssuerURL, authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, authOpenIDTokenURL: this.authOpenIDTokenURL, @@ -247,8 +226,6 @@ class ServerSettings { toJSONForBrowser() { const json = this.toJSON() delete json.tokenSecret - delete json.authGoogleOauth20ClientID - delete json.authGoogleOauth20ClientSecret delete json.authOpenIDClientID delete json.authOpenIDClientSecret return json @@ -261,9 +238,6 @@ class ServerSettings { get authenticationSettings() { return { authActiveAuthMethods: this.authActiveAuthMethods, - authGoogleOauth20ClientID: this.authGoogleOauth20ClientID, // Do not return to client - authGoogleOauth20ClientSecret: this.authGoogleOauth20ClientSecret, // Do not return to client - authGoogleOauth20CallbackURL: this.authGoogleOauth20CallbackURL, authOpenIDIssuerURL: this.authOpenIDIssuerURL, authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, authOpenIDTokenURL: this.authOpenIDTokenURL, From d990e5b9094e13408651204589bd8886e1a90a5b Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sun, 12 Nov 2023 13:30:23 +0000 Subject: [PATCH 151/208] Add NFO metadata source --- .../components/modals/libraries/EditModal.vue | 2 +- .../libraries/LibraryScannerSettings.vue | 5 + server/objects/settings/LibrarySettings.js | 4 +- server/scanner/BookScanner.js | 11 ++- server/scanner/LibraryItemScanData.js | 5 + server/scanner/NfoFileScanner.js | 48 ++++++++++ server/utils/parsers/parseNfoMetadata.js | 94 +++++++++++++++++++ 7 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 server/scanner/NfoFileScanner.js create mode 100644 server/utils/parsers/parseNfoMetadata.js diff --git a/client/components/modals/libraries/EditModal.vue b/client/components/modals/libraries/EditModal.vue index 5bcdabede..2a68dd639 100644 --- a/client/components/modals/libraries/EditModal.vue +++ b/client/components/modals/libraries/EditModal.vue @@ -127,7 +127,7 @@ export default { skipMatchingMediaWithIsbn: false, autoScanCronExpression: null, hideSingleBookSeries: false, - metadataPrecedence: ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] + metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] } } }, diff --git a/client/components/modals/libraries/LibraryScannerSettings.vue b/client/components/modals/libraries/LibraryScannerSettings.vue index 215f79b55..253d4e6b0 100644 --- a/client/components/modals/libraries/LibraryScannerSettings.vue +++ b/client/components/modals/libraries/LibraryScannerSettings.vue @@ -64,6 +64,11 @@ export default { name: 'Audio file meta tags', include: true }, + nfoFile: { + id: 'nfoFile', + name: 'NFO file', + include: true + }, txtFiles: { id: 'txtFiles', name: 'desc.txt & reader.txt files', diff --git a/server/objects/settings/LibrarySettings.js b/server/objects/settings/LibrarySettings.js index b734b6bf4..10ee19e02 100644 --- a/server/objects/settings/LibrarySettings.js +++ b/server/objects/settings/LibrarySettings.js @@ -9,7 +9,7 @@ class LibrarySettings { this.autoScanCronExpression = null this.audiobooksOnly = false this.hideSingleBookSeries = false // Do not show series that only have 1 book - this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] + this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] if (settings) { this.construct(settings) @@ -28,7 +28,7 @@ class LibrarySettings { this.metadataPrecedence = [...settings.metadataPrecedence] } else { // Added in v2.4.5 - this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] + this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] } } diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 282155f26..48e8529ad 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -18,6 +18,7 @@ const BookFinder = require('../finders/BookFinder') const LibraryScan = require("./LibraryScan") const OpfFileScanner = require('./OpfFileScanner') +const NfoFileScanner = require('./NfoFileScanner') const AbsMetadataFileScanner = require('./AbsMetadataFileScanner') /** @@ -593,7 +594,7 @@ class BookScanner { } const bookMetadataSourceHandler = new BookScanner.BookMetadataSourceHandler(bookMetadata, audioFiles, libraryItemData, libraryScan, existingLibraryItemId) - const metadataPrecedence = librarySettings.metadataPrecedence || ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] + const metadataPrecedence = librarySettings.metadataPrecedence || ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] libraryScan.addLog(LogLevel.DEBUG, `"${bookMetadata.title}" Getting metadata with precedence [${metadataPrecedence.join(', ')}]`) for (const metadataSource of metadataPrecedence) { if (bookMetadataSourceHandler[metadataSource]) { @@ -649,6 +650,14 @@ class BookScanner { AudioFileScanner.setBookMetadataFromAudioMetaTags(bookTitle, this.audioFiles, this.bookMetadata, this.libraryScan) } + /** + * Metadata from .nfo file + */ + async nfoFile() { + if (!this.libraryItemData.metadataNfoLibraryFile) return + await NfoFileScanner.scanBookNfoFile(this.libraryItemData.metadataNfoLibraryFile, this.bookMetadata) + } + /** * Description from desc.txt and narrator from reader.txt */ diff --git a/server/scanner/LibraryItemScanData.js b/server/scanner/LibraryItemScanData.js index 576280c83..b604e4d7e 100644 --- a/server/scanner/LibraryItemScanData.js +++ b/server/scanner/LibraryItemScanData.js @@ -132,6 +132,11 @@ class LibraryItemScanData { return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.opf') } + /** @type {LibraryItem.LibraryFileObject} */ + get metadataNfoLibraryFile() { + return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.nfo') + } + /** * * @param {LibraryItem} existingLibraryItem diff --git a/server/scanner/NfoFileScanner.js b/server/scanner/NfoFileScanner.js new file mode 100644 index 000000000..e450b5c30 --- /dev/null +++ b/server/scanner/NfoFileScanner.js @@ -0,0 +1,48 @@ +const { parseNfoMetadata } = require('../utils/parsers/parseNfoMetadata') +const { readTextFile } = require('../utils/fileUtils') + +class NfoFileScanner { + constructor() { } + + /** + * Parse metadata from .nfo file found in library scan and update bookMetadata + * + * @param {import('../models/LibraryItem').LibraryFileObject} nfoLibraryFileObj + * @param {Object} bookMetadata + */ + async scanBookNfoFile(nfoLibraryFileObj, bookMetadata) { + const nfoText = await readTextFile(nfoLibraryFileObj.metadata.path) + const nfoMetadata = nfoText ? await parseNfoMetadata(nfoText) : null + if (nfoMetadata) { + for (const key in nfoMetadata) { + if (key === 'tags') { // Add tags only if tags are empty + if (nfoMetadata.tags.length) { + bookMetadata.tags = nfoMetadata.tags + } + } else if (key === 'genres') { // Add genres only if genres are empty + if (nfoMetadata.genres.length) { + bookMetadata.genres = nfoMetadata.genres + } + } else if (key === 'authors') { + if (nfoMetadata.authors?.length) { + bookMetadata.authors = nfoMetadata.authors + } + } else if (key === 'narrators') { + if (nfoMetadata.narrators?.length) { + bookMetadata.narrators = nfoMetadata.narrators + } + } else if (key === 'series') { + if (nfoMetadata.series) { + bookMetadata.series = [{ + name: nfoMetadata.series, + sequence: nfoMetadata.sequence || null + }] + } + } else if (nfoMetadata[key] && key !== 'sequence') { + bookMetadata[key] = nfoMetadata[key] + } + } + } + } +} +module.exports = new NfoFileScanner() \ No newline at end of file diff --git a/server/utils/parsers/parseNfoMetadata.js b/server/utils/parsers/parseNfoMetadata.js new file mode 100644 index 000000000..a7fbbceb4 --- /dev/null +++ b/server/utils/parsers/parseNfoMetadata.js @@ -0,0 +1,94 @@ +function parseNfoMetadata(nfoText) { + if (!nfoText) return null + const lines = nfoText.split(/\r?\n/) + const metadata = {} + let insideBookDescription = false + lines.forEach(line => { + if (line.search(/^\s*book description\s*$/i) !== -1) { + insideBookDescription = true + return + } + if (insideBookDescription) { + if (line.search(/^\s*=+\s*$/i) !== -1) return + metadata.description = metadata.description || '' + metadata.description += line + '\n' + return + } + const match = line.match(/^(.*?):(.*)$/) + if (match) { + const key = match[1].toLowerCase().trim() + const value = match[2].trim() + if (!value) return + switch (key) { + case 'title': + { + const titleMatch = value.match(/^(.*?):(.*)$/) + if (titleMatch) { + metadata.title = titleMatch[1].trim() + metadata.subtitle = titleMatch[2].trim() + } else { + metadata.title = value + } + } + break + case 'author': + metadata.authors = value.split(/\s*,\s*/) + break + case 'narrator': + case 'read by': + metadata.narrators = value.split(/\s*,\s*/) + break + case 'series name': + metadata.series = value + break + case 'genre': + metadata.genres = value.split(/\s*,\s*/) + break + case 'tags': + metadata.tags = value.split(/\s*,\s*/) + break + case 'copyright': + case 'audible.com release': + case 'audiobook copyright': + case 'book copyright': + case 'recording copyright': + case 'release date': + case 'date': + { + const year = extractYear(value) + if (year) { + metadata.publishedYear = year + } + } + break; + case 'position in series': + metadata.sequence = value + break + case 'unabridged': + metadata.abridged = value.toLowerCase() === 'yes' ? false : true + break + case 'abridged': + metadata.abridged = value.toLowerCase() === 'no' ? false : true + break + case 'publisher': + metadata.publisher = value + break + case 'asin': + metadata.asin = value + break + case 'isbn': + case 'isbn-10': + case 'isbn-13': + metadata.isbn = value + break + } + } + }) + return metadata +} +module.exports = { parseNfoMetadata } + +function extractYear(str) { + const match = str.match(/\d{4}/g) + return match ? match[match.length-1] : null +} \ No newline at end of file From 4dec8c265d15fef8ea65b74690354684e51eb369 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Fri, 17 Nov 2023 08:47:40 +0200 Subject: [PATCH 152/208] Add ApiCacheManager --- server/managers/ApiCacheManager | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 server/managers/ApiCacheManager diff --git a/server/managers/ApiCacheManager b/server/managers/ApiCacheManager new file mode 100644 index 000000000..9d80fdb2e --- /dev/null +++ b/server/managers/ApiCacheManager @@ -0,0 +1,43 @@ +const { LRUCache } = require('lru-cache') +const Logger = require('../Logger') +const { measure } = require('../utils/timing') + +class ApiCacheManager { + constructor() { + this.options = { + max: 1000, + maxSize: 10 * 1000 * 1000, + sizeCalculation: item => item.length, + } + } + + init() { + this.cache = new LRUCache(this.options) + } + + get middleware() { + return (req, res, next) => { + measure('ApiCacheManager.middleware', () => { + const key = req.originalUrl || req.url + Logger.debug(`[ApiCacheManager] Cache key: ${key}`) + Logger.debug(`[ApiCacheManager] Cache: ${this.cache} count: ${this.cache.size} size: ${this.cache.calculatedSize}`) + const cached = this.cache.get(key) + if (cached) { + Logger.debug(`[ApiCacheManager] Cache hit: ${key}`) + res.send(cached) + return + } + res.sendResponse = res.send + res.send = (body) => { + Logger.debug(`[ApiCacheManager] Cache miss: ${key}`) + measure('ApiCacheManager.middleware: res.send', () => { + this.cache.set(key, body) + res.sendResponse(body) + }) + } + next() + }) + } + } +} +module.exports = ApiCacheManager \ No newline at end of file From f22f3361d5f8e853fce29290909aa62ea9ab3c15 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Fri, 17 Nov 2023 08:48:09 +0200 Subject: [PATCH 153/208] Add timing utils --- server/utils/timing.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 server/utils/timing.js diff --git a/server/utils/timing.js b/server/utils/timing.js new file mode 100644 index 000000000..af019e78b --- /dev/null +++ b/server/utils/timing.js @@ -0,0 +1,27 @@ +const { performance } = require('perf_hooks') +const Logger = require('../Logger') + + +async function measure(tag, func) { + const start = performance.now() + const result = await func() + const end = performance.now() + Logger.debug(`[${tag}] Time elapsed: ${(end - start) | 0} ms`) + return result +} + +function measureMiddleware(req, res, next) { + const start = performance.now() + res.on('finish', () => { + const end = performance.now() + if (!req.originalUrl.includes('cover')) + Logger.debug(`[${req.method} ${req.originalUrl}] Finish: Time elapsed: ${(end - start) | 0} ms`) + }) + res.on('close', () => { + const end = performance.now() + if (!req.originalUrl.includes('cover')) + Logger.debug(`[${req.method} ${req.originalUrl}] Close: Time elapsed: ${(end - start) | 0} ms`) + }) + next() +} +module.exports = { measure, measureMiddleware } \ No newline at end of file From 6a722102c5f2b0e948162710652d00945ee4c475 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Fri, 17 Nov 2023 08:49:40 +0200 Subject: [PATCH 154/208] Use ApiCacheManager & timing middleware --- server/Server.js | 5 +++++ server/routers/ApiRouter.js | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/server/Server.js b/server/Server.js index ba63b2bde..f90e9754a 100644 --- a/server/Server.js +++ b/server/Server.js @@ -31,7 +31,9 @@ const PodcastManager = require('./managers/PodcastManager') const AudioMetadataMangaer = require('./managers/AudioMetadataManager') const RssFeedManager = require('./managers/RssFeedManager') const CronManager = require('./managers/CronManager') +const ApiCacheManager = require('./managers/ApiCacheManager') const LibraryScanner = require('./scanner/LibraryScanner') +const { measureMiddleware } = require('./utils/timing') class Server { constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { @@ -67,6 +69,7 @@ class Server { this.audioMetadataManager = new AudioMetadataMangaer() this.rssFeedManager = new RssFeedManager() this.cronManager = new CronManager(this.podcastManager) + this.apiCacheManager = new ApiCacheManager() // Routers this.apiRouter = new ApiRouter(this) @@ -110,6 +113,7 @@ class Server { const libraries = await Database.libraryModel.getAllOldLibraries() await this.cronManager.init(libraries) + this.apiCacheManager.init() if (Database.serverSettings.scannerDisableWatcher) { Logger.info(`[Server] Watcher is disabled`) @@ -130,6 +134,7 @@ class Server { this.server = http.createServer(app) + router.use(measureMiddleware) router.use(this.auth.cors) router.use(fileUpload({ defCharset: 'utf8', diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index bb91e9b5e..43c326287 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -32,6 +32,7 @@ const MiscController = require('../controllers/MiscController') const Author = require('../objects/entities/Author') const Series = require('../objects/entities/Series') +const { measureMiddleware } = require('../utils/timing') class ApiRouter { constructor(Server) { @@ -47,6 +48,7 @@ class ApiRouter { this.cronManager = Server.cronManager this.notificationManager = Server.notificationManager this.emailManager = Server.emailManager + this.apiCacheManager = Server.apiCacheManager this.router = express() this.router.disable('x-powered-by') @@ -54,16 +56,17 @@ class ApiRouter { } init() { + const cacheMiddleware = this.apiCacheManager.middleware // // Library Routes // this.router.post('/libraries', LibraryController.create.bind(this)) this.router.get('/libraries', LibraryController.findAll.bind(this)) - this.router.get('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.findOne.bind(this)) + this.router.get('/libraries/:id', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.findOne.bind(this)) this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this)) this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this)) - this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this)) + this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getLibraryItems.bind(this)) this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this)) this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this)) this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this)) From 4299627f5f58b9ab50835d142aa7350120ef774a Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Fri, 17 Nov 2023 08:54:16 +0200 Subject: [PATCH 155/208] Add lru-cache dependency --- package-lock.json | 180 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 3 +- 2 files changed, 177 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 888c3beb4..fedfddc6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "express": "^4.17.1", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", + "lru-cache": "^10.0.2", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "sequelize": "^6.32.1", @@ -53,6 +54,17 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -91,6 +103,18 @@ "semver": "^7.3.5" } }, + "node_modules/@npmcli/fs/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@npmcli/fs/node_modules/semver": { "version": "7.5.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", @@ -429,6 +453,18 @@ "node": ">= 10" } }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -1315,6 +1351,17 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lru-cache": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", + "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/lru-cache/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", @@ -1325,6 +1372,20 @@ "node": ">=10" } }, + "node_modules/lru-cache/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -1374,6 +1435,18 @@ "node": ">= 10" } }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1647,6 +1720,18 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/node-gyp/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-gyp/node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -2148,6 +2233,17 @@ } } }, + "node_modules/sequelize/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sequelize/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2734,6 +2830,14 @@ "tar": "^6.1.11" }, "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -2762,6 +2866,15 @@ "semver": "^7.3.5" }, "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + }, "semver": { "version": "7.5.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", @@ -3035,6 +3148,17 @@ "ssri": "^8.0.1", "tar": "^6.0.2", "unique-filename": "^1.1.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "call-bind": { @@ -3696,11 +3820,29 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", + "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", "requires": { - "yallist": "^4.0.0" + "semver": "^7.3.5" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "make-dir": { @@ -3740,6 +3882,17 @@ "promise-retry": "^2.0.1", "socks-proxy-agent": "^6.0.0", "ssri": "^8.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "media-typer": { @@ -3933,6 +4086,15 @@ "wide-align": "^1.1.5" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -4269,6 +4431,14 @@ "ms": "2.1.2" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4704,4 +4874,4 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 4bef0e425..dafd8907b 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "express": "^4.17.1", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", + "lru-cache": "^10.0.2", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "sequelize": "^6.32.1", @@ -46,4 +47,4 @@ "devDependencies": { "nodemon": "^2.0.20" } -} \ No newline at end of file +} From 80e061115f10c47d1bb2720fff3661542af338f2 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 18 Nov 2023 13:41:08 -0600 Subject: [PATCH 156/208] Add remove semicolons to .vscode settings, update BookFinder.test formatting --- .vscode/settings.json | 3 +- test/server/finders/BookFinder.test.js | 634 ++++++++++++------------- 2 files changed, 319 insertions(+), 318 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2fb8b48fb..397b9618e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,6 @@ }, "editor.formatOnSave": true, "editor.detectIndentation": true, - "editor.tabSize": 2 + "editor.tabSize": 2, + "javascript.format.semicolons": "remove" } \ No newline at end of file diff --git a/test/server/finders/BookFinder.test.js b/test/server/finders/BookFinder.test.js index 01dcb5758..2728f174a 100644 --- a/test/server/finders/BookFinder.test.js +++ b/test/server/finders/BookFinder.test.js @@ -1,344 +1,344 @@ -const sinon = require('sinon'); -const chai = require('chai'); -const expect = chai.expect; -const bookFinder = require('../../../server/finders/BookFinder'); +const sinon = require('sinon') +const chai = require('chai') +const expect = chai.expect +const bookFinder = require('../../../server/finders/BookFinder') const { LogLevel } = require('../../../server/utils/constants') const Logger = require('../../../server/Logger') Logger.setLogLevel(LogLevel.INFO) - describe('TitleCandidates', () => { - describe('cleanAuthor non-empty', () => { - let titleCandidates; - const cleanAuthor = 'leo tolstoy'; +describe('TitleCandidates', () => { + describe('cleanAuthor non-empty', () => { + let titleCandidates + const cleanAuthor = 'leo tolstoy' - beforeEach(() => { - titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor); - }); + beforeEach(() => { + titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) + }) - describe('no adds', () => { - it('returns no candidates', () => { - expect(titleCandidates.getCandidates()).to.deep.equal([]); - }) - }) - - describe('single add', () => { - [ - ['adds candidate', 'anna karenina', ['anna karenina']], - ['adds lowercased candidate', 'ANNA KARENINA', ['anna karenina']], - ['adds candidate, removing redundant spaces', 'anna karenina', ['anna karenina']], - ['adds candidate, removing author', `anna karenina by ${cleanAuthor}`, ['anna karenina']], - ['does not add empty candidate after removing author', cleanAuthor, []], - ['adds candidate, removing subtitle', 'anna karenina: subtitle', ['anna karenina']], - ['adds candidate + variant, removing "by ..."', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], - ['adds candidate + variant, removing bitrate', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], - ['adds candidate + variant, removing edition 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], - ['adds candidate + variant, removing edition 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], - ['adds candidate + variant, removing fie type', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']], - ['adds candidate + variant, removing "a novel"', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']], - ['adds candidate + variant, removing preceding/trailing numbers', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']], - ['does not add empty candidate', '', []], - ['does not add spaces-only candidate', ' ', []], - ['does not add empty variant', '1984', ['1984']], - ].forEach(([name, title, expected]) => it(name, () => { - titleCandidates.add(title); - expect(titleCandidates.getCandidates()).to.deep.equal(expected); - })); - }) - - describe('multiple adds', () => { - [ - ['demotes digits-only candidates', ['01', 'anna karenina'], ['anna karenina', '01']], - ['promotes transformed variants', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']], - ['orders by position', ['title2', 'title1'], ['title2', 'title1']], - ['dedupes candidates', ['title1', 'title1'], ['title1']], - ].forEach(([name, titles, expected]) => it(name, () => { - for (const title of titles) titleCandidates.add(title) - expect(titleCandidates.getCandidates()).to.deep.equal(expected); - })); + describe('no adds', () => { + it('returns no candidates', () => { + expect(titleCandidates.getCandidates()).to.deep.equal([]) }) }) - describe('cleanAuthor empty', () => { - let titleCandidates - let cleanAuthor = '' - - beforeEach(() => { - titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) - }) - - describe('single add', () => { - [ - ['adds a candidate', 'leo tolstoy', ['leo tolstoy']], - ].forEach(([name, title, expected]) => it(name, () => { - titleCandidates.add(title) - expect(titleCandidates.getCandidates()).to.deep.equal(expected); - })) - }) + describe('single add', () => { + [ + ['adds candidate', 'anna karenina', ['anna karenina']], + ['adds lowercased candidate', 'ANNA KARENINA', ['anna karenina']], + ['adds candidate, removing redundant spaces', 'anna karenina', ['anna karenina']], + ['adds candidate, removing author', `anna karenina by ${cleanAuthor}`, ['anna karenina']], + ['does not add empty candidate after removing author', cleanAuthor, []], + ['adds candidate, removing subtitle', 'anna karenina: subtitle', ['anna karenina']], + ['adds candidate + variant, removing "by ..."', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], + ['adds candidate + variant, removing bitrate', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], + ['adds candidate + variant, removing edition 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], + ['adds candidate + variant, removing edition 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], + ['adds candidate + variant, removing fie type', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']], + ['adds candidate + variant, removing "a novel"', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']], + ['adds candidate + variant, removing preceding/trailing numbers', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']], + ['does not add empty candidate', '', []], + ['does not add spaces-only candidate', ' ', []], + ['does not add empty variant', '1984', ['1984']], + ].forEach(([name, title, expected]) => it(name, () => { + titleCandidates.add(title) + expect(titleCandidates.getCandidates()).to.deep.equal(expected) + })) + }) + + describe('multiple adds', () => { + [ + ['demotes digits-only candidates', ['01', 'anna karenina'], ['anna karenina', '01']], + ['promotes transformed variants', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']], + ['orders by position', ['title2', 'title1'], ['title2', 'title1']], + ['dedupes candidates', ['title1', 'title1'], ['title1']], + ].forEach(([name, titles, expected]) => it(name, () => { + for (const title of titles) titleCandidates.add(title) + expect(titleCandidates.getCandidates()).to.deep.equal(expected) + })) }) }) - describe('AuthorCandidates', () => { - let authorCandidates; - const audnexus = { - authorASINsRequest: sinon.stub().resolves([ - { name: 'Leo Tolstoy' }, - { name: 'Nikolai Gogol' }, - { name: 'J. K. Rowling' }, - ]), - }; - - describe('cleanAuthor is null', () => { - beforeEach(() => { - authorCandidates = new bookFinder.constructor.AuthorCandidates(null, audnexus); - }); - - describe('no adds', () => { - [ - ['returns empty author candidate', []], - ].forEach(([name, expected]) => it(name, async () => { - expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) - })) - }); - - describe('single add', () => { - [ - ['adds recognized candidate', 'nikolai gogol', ['nikolai gogol']], - ['does not add unrecognized candidate', 'fyodor dostoevsky', []], - ['adds recognized author if candidate is a superstring', 'dr. nikolai gogol', ['nikolai gogol']], - ['adds candidate if it is a substring of recognized author', 'gogol', ['gogol']], - ['adds recognized author if edit distance from candidate is small', 'nicolai gogol', ['nikolai gogol']], - ['does not add candidate if edit distance from any recognized author is large', 'nikolai google', []], - ['adds normalized recognized candidate (contains redundant spaces)', 'nikolai gogol', ['nikolai gogol']], - ['adds normalized recognized candidate (normalized initials)', 'j.k. rowling', ['j. k. rowling']], - ].forEach(([name, author, expected]) => it(name, async () => { - authorCandidates.add(author) - expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) - })); - }) - - describe('multi add', () => { - [ - ['adds recognized author candidates', ['nikolai gogol', 'leo tolstoy'], ['nikolai gogol', 'leo tolstoy']], - ['dedupes author candidates', ['nikolai gogol', 'nikolai gogol'], ['nikolai gogol']], - ].forEach(([name, authors, expected]) => it(name, async () => { - for (const author of authors) authorCandidates.add(author) - expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) - })) - }) - }); - - describe('cleanAuthor is a recognized author', () => { - const cleanAuthor = 'leo tolstoy'; - - beforeEach(() => { - authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus); - }); - - describe('no adds', () => { - [ - ['adds cleanAuthor as candidate', [cleanAuthor]], - ].forEach(([name, expected]) => it(name, async () => { - expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) - })) - }) - - describe('single add', () => { - [ - ['adds recognized candidate', 'nikolai gogol', [cleanAuthor, 'nikolai gogol']], - ['does not add candidate if it is a dupe of cleanAuthor', cleanAuthor, [cleanAuthor]], - ].forEach(([name, author, expected]) => it(name, async () => { - authorCandidates.add(author) - expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) - })) - }) - }); - - describe('cleanAuthor is an unrecognized author', () => { - const cleanAuthor = 'Fyodor Dostoevsky'; - - beforeEach(() => { - authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus); - }); - - describe('no adds', () => { - [ - ['adds cleanAuthor as candidate', [cleanAuthor]], - ].forEach(([name, expected]) => it(name, async () => { - expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) - })) - }) - - describe('single add', () => { - [ - ['adds recognized candidate and removes cleanAuthor', 'nikolai gogol', ['nikolai gogol']], - ['does not add unrecognized candidate', 'jackie chan', [cleanAuthor]], - ].forEach(([name, author, expected]) => it(name, async () => { - authorCandidates.add(author) - expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) - })) - }) - }); - - describe('cleanAuthor is unrecognized and dirty', () => { - describe('no adds', () => { - [ - ['adds aggressively cleaned cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', ['fyodor dostoevsky']], - ['adds cleanAuthor if aggresively cleaned cleanAuthor is empty', ', jackie chan', [', jackie chan']], - ].forEach(([name, cleanAuthor, expected]) => it(name, async () => { - authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) - expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) - })) - }) - - describe('single add', () => { - [ - ['adds recognized candidate and removes cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', 'nikolai gogol', ['nikolai gogol']], - ].forEach(([name, cleanAuthor, author, expected]) => it(name, async () => { - authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) - authorCandidates.add(author) - expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) - })) - }) - }); - }); - - describe('search', () => { - const t = 'title'; - const a = 'author'; - const u = 'unrecognized'; - const r = ['book']; - - const runSearchStub = sinon.stub(bookFinder, 'runSearch') - runSearchStub.resolves([]) - runSearchStub.withArgs(t, a).resolves(r); - runSearchStub.withArgs(t, u).resolves(r); - - const audnexusStub = sinon.stub(bookFinder.audnexus, 'authorASINsRequest') - audnexusStub.resolves([ { name: a } ]) + describe('cleanAuthor empty', () => { + let titleCandidates + let cleanAuthor = '' beforeEach(() => { - bookFinder.runSearch.resetHistory(); - }); + titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) + }) - describe('search title is empty', () => { - it('returns empty result', async () => { - expect(await bookFinder.search('', '', a)).to.deep.equal([]); - sinon.assert.callCount(bookFinder.runSearch, 0); - }); - }); - - describe('search title is a recognized title and search author is a recognized author', () => { - it('returns non-empty result (no fuzzy searches)', async () => { - expect(await bookFinder.search('', t, a)).to.deep.equal(r); - sinon.assert.callCount(bookFinder.runSearch, 1); - }); - }); - - describe('search title contains recognized title and search author is a recognized author', () => { + describe('single add', () => { [ - [`${t} -`], - [`${t} - ${a}`], - [`${a} - ${t}`], - [`${t}- ${a}`], - [`${t} -${a}`], - [`${t} ${a}`], - [`${a} - ${t} (unabridged)`], - [`${a} - ${t} (subtitle) - mp3`], - [`${t} {narrator} - series-01 64kbps 10:00:00`], - [`${a} - ${t} (2006) narrated by narrator [unabridged]`], - [`${t} - ${a} 2022 mp3`], - [`01 ${t}`], - [`2022_${t}_HQ`], - ].forEach(([searchTitle]) => { - it(`search('${searchTitle}', '${a}') returns non-empty result (with 1 fuzzy search)`, async () => { - expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r); - sinon.assert.callCount(bookFinder.runSearch, 2); - }); - }); + ['adds a candidate', 'leo tolstoy', ['leo tolstoy']], + ].forEach(([name, title, expected]) => it(name, () => { + titleCandidates.add(title) + expect(titleCandidates.getCandidates()).to.deep.equal(expected) + })) + }) + }) +}) +describe('AuthorCandidates', () => { + let authorCandidates + const audnexus = { + authorASINsRequest: sinon.stub().resolves([ + { name: 'Leo Tolstoy' }, + { name: 'Nikolai Gogol' }, + { name: 'J. K. Rowling' }, + ]), + } + + describe('cleanAuthor is null', () => { + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(null, audnexus) + }) + + describe('no adds', () => { + [ + ['returns empty author candidate', []], + ].forEach(([name, expected]) => it(name, async () => { + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('single add', () => { + [ + ['adds recognized candidate', 'nikolai gogol', ['nikolai gogol']], + ['does not add unrecognized candidate', 'fyodor dostoevsky', []], + ['adds recognized author if candidate is a superstring', 'dr. nikolai gogol', ['nikolai gogol']], + ['adds candidate if it is a substring of recognized author', 'gogol', ['gogol']], + ['adds recognized author if edit distance from candidate is small', 'nicolai gogol', ['nikolai gogol']], + ['does not add candidate if edit distance from any recognized author is large', 'nikolai google', []], + ['adds normalized recognized candidate (contains redundant spaces)', 'nikolai gogol', ['nikolai gogol']], + ['adds normalized recognized candidate (normalized initials)', 'j.k. rowling', ['j. k. rowling']], + ].forEach(([name, author, expected]) => it(name, async () => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('multi add', () => { + [ + ['adds recognized author candidates', ['nikolai gogol', 'leo tolstoy'], ['nikolai gogol', 'leo tolstoy']], + ['dedupes author candidates', ['nikolai gogol', 'nikolai gogol'], ['nikolai gogol']], + ].forEach(([name, authors, expected]) => it(name, async () => { + for (const author of authors) authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }) + + describe('cleanAuthor is a recognized author', () => { + const cleanAuthor = 'leo tolstoy' + + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + }) + + describe('no adds', () => { + [ + ['adds cleanAuthor as candidate', [cleanAuthor]], + ].forEach(([name, expected]) => it(name, async () => { + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('single add', () => { + [ + ['adds recognized candidate', 'nikolai gogol', [cleanAuthor, 'nikolai gogol']], + ['does not add candidate if it is a dupe of cleanAuthor', cleanAuthor, [cleanAuthor]], + ].forEach(([name, author, expected]) => it(name, async () => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }) + + describe('cleanAuthor is an unrecognized author', () => { + const cleanAuthor = 'Fyodor Dostoevsky' + + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + }) + + describe('no adds', () => { + [ + ['adds cleanAuthor as candidate', [cleanAuthor]], + ].forEach(([name, expected]) => it(name, async () => { + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('single add', () => { + [ + ['adds recognized candidate and removes cleanAuthor', 'nikolai gogol', ['nikolai gogol']], + ['does not add unrecognized candidate', 'jackie chan', [cleanAuthor]], + ].forEach(([name, author, expected]) => it(name, async () => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }) + + describe('cleanAuthor is unrecognized and dirty', () => { + describe('no adds', () => { + [ + ['adds aggressively cleaned cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', ['fyodor dostoevsky']], + ['adds cleanAuthor if aggresively cleaned cleanAuthor is empty', ', jackie chan', [', jackie chan']], + ].forEach(([name, cleanAuthor, expected]) => it(name, async () => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('single add', () => { + [ + ['adds recognized candidate and removes cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', 'nikolai gogol', ['nikolai gogol']], + ].forEach(([name, cleanAuthor, author, expected]) => it(name, async () => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }) +}) + +describe('search', () => { + const t = 'title' + const a = 'author' + const u = 'unrecognized' + const r = ['book'] + + const runSearchStub = sinon.stub(bookFinder, 'runSearch') + runSearchStub.resolves([]) + runSearchStub.withArgs(t, a).resolves(r) + runSearchStub.withArgs(t, u).resolves(r) + + const audnexusStub = sinon.stub(bookFinder.audnexus, 'authorASINsRequest') + audnexusStub.resolves([{ name: a }]) + + beforeEach(() => { + bookFinder.runSearch.resetHistory() + }) + + describe('search title is empty', () => { + it('returns empty result', async () => { + expect(await bookFinder.search('', '', a)).to.deep.equal([]) + sinon.assert.callCount(bookFinder.runSearch, 0) + }) + }) + + describe('search title is a recognized title and search author is a recognized author', () => { + it('returns non-empty result (no fuzzy searches)', async () => { + expect(await bookFinder.search('', t, a)).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 1) + }) + }) + + describe('search title contains recognized title and search author is a recognized author', () => { + [ + [`${t} -`], + [`${t} - ${a}`], + [`${a} - ${t}`], + [`${t}- ${a}`], + [`${t} -${a}`], + [`${t} ${a}`], + [`${a} - ${t} (unabridged)`], + [`${a} - ${t} (subtitle) - mp3`], + [`${t} {narrator} - series-01 64kbps 10:00:00`], + [`${a} - ${t} (2006) narrated by narrator [unabridged]`], + [`${t} - ${a} 2022 mp3`], + [`01 ${t}`], + [`2022_${t}_HQ`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns non-empty result (with 1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 2) + }) + }); + + [ + [`s-01 - ${t} (narrator) 64kbps 10:00:00`], + [`${a} - series 01 - ${t}`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns non-empty result (with 2 fuzzy searches)`, async () => { + expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 3) + }) + }); + + [ + [`${t}-${a}`], + [`${t} junk`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns an empty result`, async () => { + expect(await bookFinder.search('', searchTitle, a)).to.deep.equal([]) + }) + }) + + describe('maxFuzzySearches = 0', () => { + [ + [`${t} - ${a}`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns an empty result (with no fuzzy searches)`, async () => { + expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 0 })).to.deep.equal([]) + sinon.assert.callCount(bookFinder.runSearch, 1) + }) + }) + }) + + describe('maxFuzzySearches = 1', () => { [ [`s-01 - ${t} (narrator) 64kbps 10:00:00`], [`${a} - series 01 - ${t}`], ].forEach(([searchTitle]) => { - it(`search('${searchTitle}', '${a}') returns non-empty result (with 2 fuzzy searches)`, async () => { - expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r); - sinon.assert.callCount(bookFinder.runSearch, 3); - }); - }); + it(`search('${searchTitle}', '${a}') returns an empty result (1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 1 })).to.deep.equal([]) + sinon.assert.callCount(bookFinder.runSearch, 2) + }) + }) + }) + }) - [ - [`${t}-${a}`], - [`${t} junk`], - ].forEach(([searchTitle]) => { - it(`search('${searchTitle}', '${a}') returns an empty result`, async () => { - expect(await bookFinder.search('', searchTitle, a)).to.deep.equal([]); - }); - }); - - describe('maxFuzzySearches = 0', () => { - [ - [`${t} - ${a}`], - ].forEach(([searchTitle]) => { - it(`search('${searchTitle}', '${a}') returns an empty result (with no fuzzy searches)`, async () => { - expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 0 })).to.deep.equal([]); - sinon.assert.callCount(bookFinder.runSearch, 1); - }); - }); - }); - - describe('maxFuzzySearches = 1', () => { - [ - [`s-01 - ${t} (narrator) 64kbps 10:00:00`], - [`${a} - series 01 - ${t}`], - ].forEach(([searchTitle]) => { - it(`search('${searchTitle}', '${a}') returns an empty result (1 fuzzy search)`, async () => { - expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 1 })).to.deep.equal([]); - sinon.assert.callCount(bookFinder.runSearch, 2); - }); - }); - }); + describe('search title contains recognized title and search author is empty', () => { + [ + [`${t} - ${a}`], + [`${a} - ${t}`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '') returns a non-empty result (1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, '')).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 2) + }) }); - describe('search title contains recognized title and search author is empty', () => { - [ - [`${t} - ${a}`], - [`${a} - ${t}`], - ].forEach(([searchTitle]) => { - it(`search('${searchTitle}', '') returns a non-empty result (1 fuzzy search)`, async () => { - expect(await bookFinder.search('', searchTitle, '')).to.deep.equal(r); - sinon.assert.callCount(bookFinder.runSearch, 2); - }); - }); + [ + [`${t}`], + [`${t} - ${u}`], + [`${u} - ${t}`] + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '') returns an empty result`, async () => { + expect(await bookFinder.search('', searchTitle, '')).to.deep.equal([]) + }) + }) + }) - [ - [`${t}`], - [`${t} - ${u}`], - [`${u} - ${t}`] - ].forEach(([searchTitle]) => { - it(`search('${searchTitle}', '') returns an empty result`, async () => { - expect(await bookFinder.search('', searchTitle, '')).to.deep.equal([]); - }); - }); + describe('search title contains recognized title and search author is an unrecognized author', () => { + [ + [`${t} - ${u}`], + [`${u} - ${t}`] + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${u}') returns a non-empty result (1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 2) + }) }); - describe('search title contains recognized title and search author is an unrecognized author', () => { - [ - [`${t} - ${u}`], - [`${u} - ${t}`] - ].forEach(([searchTitle]) => { - it(`search('${searchTitle}', '${u}') returns a non-empty result (1 fuzzy search)`, async () => { - expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r); - sinon.assert.callCount(bookFinder.runSearch, 2); - }); - }); - - [ - [`${t}`] - ].forEach(([searchTitle]) => { - it(`search('${searchTitle}', '${u}') returns a non-empty result (no fuzzy search)`, async () => { - expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r); - sinon.assert.callCount(bookFinder.runSearch, 1); - }); - }); - }); - }); + [ + [`${t}`] + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${u}') returns a non-empty result (no fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 1) + }) + }) + }) +}) From 4c2c320b9d5e3d139716dbdb68df50455e0137ff Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 19 Nov 2023 11:32:48 -0600 Subject: [PATCH 157/208] Remove global CORS for api endpoints and setup temp CORS check for ebook endpoint --- server/Auth.js | 12 ------------ server/Server.js | 30 +++++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 06db47a8d..4c7b8d219 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -17,18 +17,6 @@ class Auth { constructor() { } - static cors(req, res, next) { - res.header('Access-Control-Allow-Origin', '*') - res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS') - res.header('Access-Control-Allow-Headers', '*') - res.header('Access-Control-Allow-Credentials', true) - if (req.method === 'OPTIONS') { - res.sendStatus(200) - } else { - next() - } - } - /** * Inializes all passportjs strategies and other passportjs ralated initialization. */ diff --git a/server/Server.js b/server/Server.js index df6c90034..1397bbd19 100644 --- a/server/Server.js +++ b/server/Server.js @@ -5,7 +5,7 @@ const http = require('http') const fs = require('./libs/fsExtra') const fileUpload = require('./libs/expressFileupload') const rateLimit = require('./libs/expressRateLimit') -const cookieParser = require("cookie-parser"); +const cookieParser = require("cookie-parser") const { version } = require('../package.json') @@ -132,6 +132,30 @@ class Server { const app = express() + /** + * @temporary + * This is necessary for the ebook API endpoint in the mobile apps + * The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests + * so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint + * @see https://ionicframework.com/docs/troubleshooting/cors + */ + app.use((req, res, next) => { + if (req.path.match(/\/api\/items\/([a-z0-9-]{36})\/ebook(\/[0-9]+)?/)) { + const allowedOrigins = ['capacitor://localhost', 'http://localhost'] + if (allowedOrigins.some(o => o === req.get('origin'))) { + res.header('Access-Control-Allow-Origin', req.get('origin')) + res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS') + res.header('Access-Control-Allow-Headers', '*') + res.header('Access-Control-Allow-Credentials', true) + if (req.method === 'OPTIONS') { + return res.sendStatus(200) + } + } + } + + next() + }) + // parse cookies in requests app.use(cookieParser()) // enable express-session @@ -163,7 +187,7 @@ class Server { useTempFiles: true, tempFileDir: Path.join(global.MetadataPath, 'tmp') })) - router.use(express.urlencoded({ extended: true, limit: "5mb" })); + router.use(express.urlencoded({ extended: true, limit: "5mb" })) router.use(express.json({ limit: "5mb" })) // Static path to generated nuxt @@ -173,7 +197,7 @@ class Server { // Static folder router.use(express.static(Path.join(global.appRoot, 'static'))) - router.use('/api', Auth.cors, this.authMiddleware.bind(this), this.apiRouter.router) + router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router) router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router) // RSS Feed temp route From 89eb857c145908ff4fe4adcc0ef4d58f1832a1ab Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 19 Nov 2023 12:57:17 -0600 Subject: [PATCH 158/208] Fix initialize openid auth strategy --- server/Auth.js | 5 +++++ server/controllers/MiscController.js | 10 +++++----- server/objects/settings/ServerSettings.js | 23 ++++++++++++++--------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 4c7b8d219..261c0854e 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -69,6 +69,11 @@ class Auth { * Passport use OpenIDClient.Strategy */ initAuthStrategyOpenID() { + if (!Database.serverSettings.isOpenIDAuthSettingsValid) { + Logger.error(`[Auth] Cannot init openid auth strategy - invalid settings`) + return + } + const openIdIssuerClient = new OpenIDClient.Issuer({ issuer: global.ServerSettings.authOpenIDIssuerURL, authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL, diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 11adf3e92..267db5c8e 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -556,10 +556,10 @@ class MiscController { switch (type) { case 'add': this.watcher.onFileAdded(libraryId, path) - break; + break case 'unlink': this.watcher.onFileRemoved(libraryId, path) - break; + break case 'rename': const oldPath = req.body.oldPath if (!oldPath) { @@ -567,7 +567,7 @@ class MiscController { return res.sendStatus(400) } this.watcher.onFileRename(libraryId, oldPath, path) - break; + break default: Logger.error(`[MiscController] Invalid type for updateWatchedPath. type: "${type}"`) return res.sendStatus(400) @@ -670,6 +670,8 @@ class MiscController { } if (hasUpdates) { + await Database.updateServerSettings() + // Use/unuse auth methods Database.serverSettings.supportedAuthMethods.forEach((authMethod) => { if (originalAuthMethods.includes(authMethod) && !Database.serverSettings.authActiveAuthMethods.includes(authMethod)) { @@ -682,8 +684,6 @@ class MiscController { this.auth.useAuthStrategy(authMethod) } }) - - await Database.updateServerSettings() } res.json({ diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index df5e71f13..bf3db5577 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -133,15 +133,7 @@ class ServerSettings { // remove uninitialized methods // OpenID - if (this.authActiveAuthMethods.includes('openid') && ( - !this.authOpenIDIssuerURL || - !this.authOpenIDAuthorizationURL || - !this.authOpenIDTokenURL || - !this.authOpenIDUserInfoURL || - !this.authOpenIDJwksURL || - !this.authOpenIDClientID || - !this.authOpenIDClientSecret - )) { + if (this.authActiveAuthMethods.includes('openid') && !this.isOpenIDAuthSettingsValid) { this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('openid', 0), 1) } @@ -235,6 +227,19 @@ class ServerSettings { return ['local', 'openid'] } + /** + * Auth settings required for openid to be valid + */ + get isOpenIDAuthSettingsValid() { + return this.authOpenIDIssuerURL && + this.authOpenIDAuthorizationURL && + this.authOpenIDTokenURL && + this.authOpenIDUserInfoURL && + this.authOpenIDJwksURL && + this.authOpenIDClientID && + this.authOpenIDClientSecret + } + get authenticationSettings() { return { authActiveAuthMethods: this.authActiveAuthMethods, From 91fa78d740561405597c49d7c7d02f7441175748 Mon Sep 17 00:00:00 2001 From: Lars Kiesow <lkiesow@uos.de> Date: Sun, 19 Nov 2023 20:36:04 +0100 Subject: [PATCH 159/208] Add milliseconds to logging This patch adds milliseconds to the time string used for logging. This helps when debugging some timing issues and should have no real negative side effect. --- server/Logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Logger.js b/server/Logger.js index 19e657b41..5eb33a244 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -11,7 +11,7 @@ class Logger { } get timestamp() { - return date.format(new Date(), 'YYYY-MM-DD HH:mm:ss') + return date.format(new Date(), 'YYYY-MM-DD HH:mm:ss.SSS') } get levelString() { From dcbfc963c1190193acb89bfa48e5181d0cff1371 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 19 Nov 2023 13:38:09 -0600 Subject: [PATCH 160/208] Update protocol for redirect_uri in openid strategy to work for reverse proxies --- server/Auth.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/Auth.js b/server/Auth.js index 261c0854e..a0f791ca7 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -282,7 +282,8 @@ class Auth { // We need to call the client manually, because the strategy does not support forwarding the code challenge // for API or mobile clients const oidcStrategy = passport._strategy('openid-client') - oidcStrategy._params.redirect_uri = new URL(`${req.protocol}://${req.get('host')}/auth/openid/callback`).toString() + const protocol = req.secure ? 'https' : 'http' + oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString() const client = oidcStrategy._client const sessionKey = oidcStrategy._key From aa933df525f3073f17312b4a89fa2b4eaf1f58a0 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 19 Nov 2023 14:00:39 -0600 Subject: [PATCH 161/208] Update oidc redirect_uri to check x-forwarded-proto header for proxies --- server/Auth.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/Auth.js b/server/Auth.js index a0f791ca7..76fbc66b7 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -282,8 +282,9 @@ class Auth { // We need to call the client manually, because the strategy does not support forwarding the code challenge // for API or mobile clients const oidcStrategy = passport._strategy('openid-client') - const protocol = req.secure ? 'https' : 'http' + const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http' oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString() + Logger.debug(`[Auth] Set oidc redirect_uri=${oidcStrategy._params.redirect_uri}`) const client = oidcStrategy._client const sessionKey = oidcStrategy._key From 7b6aa3ba5a720a893d22bbc558137f9b0fd6bb5b Mon Sep 17 00:00:00 2001 From: Lars Kiesow <lkiesow@uos.de> Date: Sun, 19 Nov 2023 21:00:54 +0100 Subject: [PATCH 162/208] Allow enabling dev logs This patch allows users to enable dev logs on production systems by setting the `HIDE_DEV_LOGS` environment variable. Before, you could only use this on a non-production environment. On production, the logs would be disabled. This patch changes the behavior and uses the `NODE_ENV` only as default. On production they are disabled if `HIDE_DEV_LOGS` is undefined but can be enabled by setting `HIDE_DEV_LOGS=0` on dev, they are enabled if undefined, but can be disabled by setting `HIDE_DEV_LOGS=1`. --- server/Logger.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/Logger.js b/server/Logger.js index 19e657b41..9909778f5 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -5,6 +5,7 @@ class Logger { constructor() { this.isDev = process.env.NODE_ENV !== 'production' this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE + this.hideDevLogs = process.env.HIDE_DEV_LOGS === undefined ? !this.isDev : process.env.HIDE_DEV_LOGS === '1' this.socketListeners = [] this.logManager = null @@ -92,7 +93,7 @@ class Logger { * @param {...any} args */ dev(...args) { - if (!this.isDev || process.env.HIDE_DEV_LOGS === '1') return + if (this.hideDevLogs) return console.log(`[${this.timestamp}] DEV:`, ...args) } From 3cc900ffbfc3c3c6d2f08e64a717e73c62c2d82b Mon Sep 17 00:00:00 2001 From: Kieran Eglin <kieran.eglin@gmail.com> Date: Mon, 20 Nov 2023 08:51:00 -0800 Subject: [PATCH 163/208] Adds fetching book data on upload --- client/components/cards/ItemUploadCard.vue | 54 +++++++++++++-- client/pages/upload/index.vue | 81 ++++++++++++++++++---- client/strings/en-us.json | 4 +- 3 files changed, 119 insertions(+), 20 deletions(-) diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index 21d97b203..68b77d56a 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -8,6 +8,12 @@ <span class="text-base text-white text-opacity-80 font-mono material-icons">close</span> </div> + <div v-if="!isPodcast" + class="w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" + @click="fetchMetadata"> + <span class="text-base text-white text-opacity-80 font-mono material-icons">refresh</span> + </div> + <template v-if="!uploadSuccess && !uploadFailed"> <widgets-alert v-if="error" type="error"> <p class="text-base">{{ error }}</p> @@ -48,8 +54,8 @@ <p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p> </widgets-alert> - <div v-if="isUploading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20"> - <ui-loading-indicator :text="$strings.MessageUploading" /> + <div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20"> + <ui-loading-indicator :text="nonInteractionLabel" /> </div> </div> </template> @@ -61,10 +67,11 @@ export default { props: { item: { type: Object, - default: () => {} + default: () => { } }, mediaType: String, - processing: Boolean + processing: Boolean, + provider: String }, data() { return { @@ -76,7 +83,8 @@ export default { error: '', isUploading: false, uploadFailed: false, - uploadSuccess: false + uploadSuccess: false, + isFetchingMetadata: false } }, computed: { @@ -94,6 +102,16 @@ export default { } else { return this.itemData.title } + }, + isNonInteractable() { + return this.isUploading || this.isFetchingMetadata + }, + nonInteractionLabel() { + if (this.isUploading) { + return this.$strings.MessageUploading + } else if (this.isFetchingMetadata) { + return this.$strings.LabelFetchingMetadata + } } }, methods: { @@ -105,6 +123,30 @@ export default { titleUpdated() { this.error = '' }, + async fetchMetadata() { + if (!this.itemData.title.trim().length) { + return + } + + this.isFetchingMetadata = true + + try { + const searchQueryString = `title=${this.itemData.title}&author=${this.itemData.author}&provider=${this.provider}` + const [bestCandidate, ..._rest] = await this.$axios.$get(`/api/search/books?${searchQueryString}`) + + this.itemData = { + ...this.itemData, + title: bestCandidate?.title, + author: bestCandidate?.author, + series: (bestCandidate?.series || [])[0]?.series + } + } catch (e) { + console.error('Failed', e) + // TODO: do something with the error? + } finally { + this.isFetchingMetadata = false + } + }, getData() { if (!this.itemData.title) { this.error = 'Must have a title' @@ -128,4 +170,4 @@ export default { } } } -</script> \ No newline at end of file +</script> diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue index 09a9008bc..bf3cf5845 100644 --- a/client/pages/upload/index.vue +++ b/client/pages/upload/index.vue @@ -14,6 +14,14 @@ </div> </div> + <div v-if="!selectedLibraryIsPodcast" class="flex items-center py-2"> + <ui-toggle-switch v-model="fetchMetadata.enabled" /> + <p class="pl-4 text-base">{{ $strings.LabelAutoFetchMetadata }}</p> + <div class="flex-grow ml-4"> + <ui-dropdown v-model="fetchMetadata.provider" :items="providers" :label="$strings.LabelProvider" :disabled="!fetchMetadata.enabled" /> + </div> + </div> + <widgets-alert v-if="error" type="error"> <p class="text-lg">{{ error }}</p> </widgets-alert> @@ -61,9 +69,16 @@ </widgets-alert> <!-- Item Upload cards --> - <template v-for="item in items"> - <cards-item-upload-card :ref="`itemCard-${item.index}`" :key="item.index" :media-type="selectedLibraryMediaType" :item="item" :processing="processing" @remove="removeItem(item)" /> - </template> + <cards-item-upload-card + v-for="item in items" + :key="item.index" + :ref="`itemCard-${item.index}`" + :media-type="selectedLibraryMediaType" + :item="item" + :provider="fetchMetadata.provider" + :processing="processing" + @remove="removeItem(item)" + /> <!-- Upload/Reset btns --> <div v-show="items.length" class="flex justify-end pb-8 pt-4"> @@ -92,13 +107,18 @@ export default { selectedLibraryId: null, selectedFolderId: null, processing: false, - uploadFinished: false + uploadFinished: false, + fetchMetadata: { + enabled: false, + provider: 'google' + } } }, watch: { selectedLibrary(newVal) { if (newVal && !this.selectedFolderId) { this.setDefaultFolder() + this.setMetadataProvider() } } }, @@ -133,6 +153,13 @@ export default { selectedLibraryIsPodcast() { return this.selectedLibraryMediaType === 'podcast' }, + providers() { + if (this.selectedLibraryIsPodcast) return this.$store.state.scanners.podcastProviders + return this.$store.state.scanners.providers + }, + canFetchMetadata() { + return !this.selectedLibraryIsPodcast && this.fetchMetadata.enabled + }, selectedFolder() { if (!this.selectedLibrary) return null return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId) @@ -160,12 +187,16 @@ export default { } } this.setDefaultFolder() + this.setMetadataProvider() }, setDefaultFolder() { if (!this.selectedFolderId && this.selectedLibrary && this.selectedLibrary.folders.length) { this.selectedFolderId = this.selectedLibrary.folders[0].id } }, + setMetadataProvider() { + this.fetchMetadata.provider = this.$store.getters['libraries/getLibraryProvider'](this.selectedLibraryId) + }, removeItem(item) { this.items = this.items.filter((b) => b.index !== item.index) if (!this.items.length) { @@ -213,27 +244,49 @@ export default { var items = e.dataTransfer.items || [] var itemResults = await this.uploadHelpers.getItemsFromDrop(items, this.selectedLibraryMediaType) - this.setResults(itemResults) + this.onItemsSelected(itemResults) }, inputChanged(e) { if (!e.target || !e.target.files) return var _files = Array.from(e.target.files) if (_files && _files.length) { var itemResults = this.uploadHelpers.getItemsFromPicker(_files, this.selectedLibraryMediaType) - this.setResults(itemResults) + this.onItemsSelected(itemResults) } }, - setResults(itemResults) { + onItemsSelected(itemResults) { + if (this.itemSelectionSuccessful(itemResults)) { + // setTimeout ensures the new item ref is attached before this method is called + setTimeout(this.attemptMetadataFetch, 0) + } + }, + itemSelectionSuccessful(itemResults) { + console.log('Upload results', itemResults) + if (itemResults.error) { this.error = itemResults.error this.items = [] this.ignoredFiles = [] - } else { - this.error = '' - this.items = itemResults.items - this.ignoredFiles = itemResults.ignoredFiles + return false } - console.log('Upload results', itemResults) + + this.error = '' + this.items = itemResults.items + this.ignoredFiles = itemResults.ignoredFiles + return true + }, + attemptMetadataFetch() { + if (!this.canFetchMetadata) { + return false + } + + this.items.forEach((item) => { + let itemRef = this.$refs[`itemCard-${item.index}`] + + if (itemRef?.length) { + itemRef[0].fetchMetadata(this.fetchMetadata.provider) + } + }) }, updateItemCardStatus(index, status) { var ref = this.$refs[`itemCard-${index}`] @@ -346,6 +399,8 @@ export default { }, mounted() { this.selectedLibraryId = this.$store.state.libraries.currentLibraryId + this.setMetadataProvider() + this.setDefaultFolder() window.addEventListener('dragenter', this.dragenter) window.addEventListener('dragleave', this.dragleave) @@ -359,4 +414,4 @@ export default { window.removeEventListener('drop', this.drop) } } -</script> \ No newline at end of file +</script> diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 6f06ca77e..9c608e48b 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -194,6 +194,7 @@ "LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthors": "Authors", "LabelAutoDownloadEpisodes": "Auto Download Episodes", + "LabelAutoFetchMetadata": "Auto Fetch Metadata", "LabelBackToUser": "Back to User", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups", @@ -259,6 +260,7 @@ "LabelExample": "Example", "LabelExplicit": "Explicit", "LabelFeedURL": "Feed URL", + "LabelFetchingMetadata": "Fetching Metadata", "LabelFile": "File", "LabelFileBirthtime": "File Birthtime", "LabelFileModified": "File Modified", @@ -727,4 +729,4 @@ "ToastSocketFailedToConnect": "Socket failed to connect", "ToastUserDeleteFailed": "Failed to delete user", "ToastUserDeleteSuccess": "User deleted" -} \ No newline at end of file +} From 8c434703fb5b24afde800cd00de62bb235f9090b Mon Sep 17 00:00:00 2001 From: Kieran Eglin <kieran.eglin@gmail.com> Date: Mon, 20 Nov 2023 09:18:50 -0800 Subject: [PATCH 164/208] Added computed metadata check to UI dropdown --- client/pages/upload/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue index bf3cf5845..8dd139907 100644 --- a/client/pages/upload/index.vue +++ b/client/pages/upload/index.vue @@ -18,7 +18,7 @@ <ui-toggle-switch v-model="fetchMetadata.enabled" /> <p class="pl-4 text-base">{{ $strings.LabelAutoFetchMetadata }}</p> <div class="flex-grow ml-4"> - <ui-dropdown v-model="fetchMetadata.provider" :items="providers" :label="$strings.LabelProvider" :disabled="!fetchMetadata.enabled" /> + <ui-dropdown v-model="fetchMetadata.provider" :items="providers" :label="$strings.LabelProvider" :disabled="!canFetchMetadata" /> </div> </div> From 048e27f03f815c29271524b6dd3ea20c0ef29971 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 20 Nov 2023 15:41:38 -0600 Subject: [PATCH 165/208] Update:Openid auth endpoint sets the mobile flag on session to be used in the callback Co-authored-by: Denis Arnst <git@sapd.eu> --- server/Auth.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 76fbc66b7..e2053fa56 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -187,7 +187,7 @@ class Auth { * @param {import('express').Response} res */ paramsToCookies(req, res) { - if (req.query.isRest?.toLowerCase() == "true") { + if (req.query.isRest?.toLowerCase() == 'true') { // store the isRest flag to the is_rest cookie res.cookie('is_rest', req.query.isRest.toLowerCase(), { maxAge: 120000, // 2 min @@ -195,7 +195,7 @@ class Auth { }) } else { // no isRest-flag set -> set is_rest cookie to false - res.cookie('is_rest', "false", { + res.cookie('is_rest', 'false', { maxAge: 120000, // 2 min httpOnly: true }) @@ -323,7 +323,8 @@ class Auth { req.session[sessionKey] = { ...req.session[sessionKey], - ...pick(params, 'nonce', 'state', 'max_age', 'response_type') + ...pick(params, 'nonce', 'state', 'max_age', 'response_type'), + mobile: req.query.isRest?.toLowerCase() === 'true' // Used in the abs callback later } // Now get the URL to direct to From 781d4f570f2617f28fa53a4fb0a05d77bc2fd494 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Tue, 21 Nov 2023 09:11:06 +0200 Subject: [PATCH 166/208] Add test for parseNfoMetadata --- .../utils/parsers/parseNfoMetadata.test.js | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 test/server/utils/parsers/parseNfoMetadata.test.js diff --git a/test/server/utils/parsers/parseNfoMetadata.test.js b/test/server/utils/parsers/parseNfoMetadata.test.js new file mode 100644 index 000000000..91141335b --- /dev/null +++ b/test/server/utils/parsers/parseNfoMetadata.test.js @@ -0,0 +1,123 @@ +const chai = require('chai') +const expect = chai.expect +const { parseNfoMetadata } = require('../../../../server/utils/parsers/parseNfoMetadata') + +describe('parseNfoMetadata', () => { + it('returns null if nfoText is empty', () => { + const result = parseNfoMetadata('') + expect(result).to.be.null + }) + + it('parses title', () => { + const nfoText = 'Title: The Great Gatsby' + const result = parseNfoMetadata(nfoText) + expect(result.title).to.equal('The Great Gatsby') + }) + + it('parses title with subtitle', () => { + const nfoText = 'Title: The Great Gatsby: A Novel' + const result = parseNfoMetadata(nfoText) + expect(result.title).to.equal('The Great Gatsby') + expect(result.subtitle).to.equal('A Novel') + }) + + it('parses authors', () => { + const nfoText = 'Author: F. Scott Fitzgerald' + const result = parseNfoMetadata(nfoText) + expect(result.authors).to.deep.equal(['F. Scott Fitzgerald']) + }) + + it('parses multiple authors', () => { + const nfoText = 'Author: John Steinbeck, Ernest Hemingway' + const result = parseNfoMetadata(nfoText) + expect(result.authors).to.deep.equal(['John Steinbeck', 'Ernest Hemingway']) + }) + + it('parses narrators', () => { + const nfoText = 'Read by: Jake Gyllenhaal' + const result = parseNfoMetadata(nfoText) + expect(result.narrators).to.deep.equal(['Jake Gyllenhaal']) + }) + + it('parses multiple narrators', () => { + const nfoText = 'Read by: Jake Gyllenhaal, Kate Winslet' + const result = parseNfoMetadata(nfoText) + expect(result.narrators).to.deep.equal(['Jake Gyllenhaal', 'Kate Winslet']) + }) + + it('parses series name', () => { + const nfoText = 'Series Name: Harry Potter' + const result = parseNfoMetadata(nfoText) + expect(result.series).to.equal('Harry Potter') + }) + + it('parses genre', () => { + const nfoText = 'Genre: Fiction' + const result = parseNfoMetadata(nfoText) + expect(result.genres).to.deep.equal(['Fiction']) + }) + + it('parses multiple genres', () => { + const nfoText = 'Genre: Fiction, Historical' + const result = parseNfoMetadata(nfoText) + expect(result.genres).to.deep.equal(['Fiction', 'Historical']) + }) + + it('parses tags', () => { + const nfoText = 'Tags: mystery, thriller' + const result = parseNfoMetadata(nfoText) + expect(result.tags).to.deep.equal(['mystery', 'thriller']) + }) + + it('parses year from various date fields', () => { + const nfoText = 'Release Date: 2021-05-01\nBook Copyright: 2021\nRecording Copyright: 2021' + const result = parseNfoMetadata(nfoText) + expect(result.publishedYear).to.equal('2021') + }) + + it('parses position in series', () => { + const nfoText = 'Position in Series: 2' + const result = parseNfoMetadata(nfoText) + expect(result.sequence).to.equal('2') + }) + + it('parses abridged flag', () => { + const nfoText = 'Abridged: No' + const result = parseNfoMetadata(nfoText) + expect(result.abridged).to.be.false + + const nfoText2 = 'Unabridged: Yes' + const result2 = parseNfoMetadata(nfoText2) + expect(result2.abridged).to.be.false + }) + + it('parses publisher', () => { + const nfoText = 'Publisher: Penguin Random House' + const result = parseNfoMetadata(nfoText) + expect(result.publisher).to.equal('Penguin Random House') + }) + + it('parses ASIN', () => { + const nfoText = 'ASIN: B08X5JZJLH' + const result = parseNfoMetadata(nfoText) + expect(result.asin).to.equal('B08X5JZJLH') + }) + + it('parses description', () => { + const nfoText = 'Book Description\n=========\nThis is a book.\n It\'s good' + const result = parseNfoMetadata(nfoText) + expect(result.description).to.equal('This is a book.\n It\'s good\n') + }) + + it('no value', () => { + const nfoText = 'Title:' + const result = parseNfoMetadata(nfoText) + expect(result.title).to.be.undefined + }) + + it('no year value', () => { + const nfoText = "Date:0" + const result = parseNfoMetadata(nfoText) + expect(result.publishedYear).to.be.undefined + }) +}) \ No newline at end of file From 0d61e29ecf6dfe2056aa2d44cfe813850c74bcbb Mon Sep 17 00:00:00 2001 From: JBlond <leet31337@web.de> Date: Tue, 21 Nov 2023 20:30:48 +0100 Subject: [PATCH 167/208] de language translation follow up for 27497451d9847fc57b7e67b8676f35532e299b2d --- client/strings/de.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index f7cf8b68e..2b1db3aea 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -181,11 +181,11 @@ "LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu", "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", "LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu", - "LabelAdminUsersOnly": "Admin users only", + "LabelAdminUsersOnly": "Nur Admin Benutzer", "LabelAll": "Alle", "LabelAllUsers": "Alle Benutzer", - "LabelAllUsersExcludingGuests": "All users excluding guests", - "LabelAllUsersIncludingGuests": "All users including guests", + "LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen", + "LabelAllUsersIncludingGuests": "All Benutzer und Gäste", "LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden", "LabelAppend": "Anhängen", "LabelAuthor": "Autor", @@ -232,7 +232,7 @@ "LabelDeselectAll": "Alles abwählen", "LabelDevice": "Gerät", "LabelDeviceInfo": "Geräteinformationen", - "LabelDeviceIsAvailableTo": "Device is available to...", + "LabelDeviceIsAvailableTo": "Dem Geärt ist es möglich zu ...", "LabelDirectory": "Verzeichnis", "LabelDiscFromFilename": "CD aus dem Dateinamen", "LabelDiscFromMetadata": "CD aus den Metadaten", @@ -398,7 +398,7 @@ "LabelSeason": "Staffel", "LabelSelectAllEpisodes": "Alle Episoden auswählen", "LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt", - "LabelSelectUsers": "Select users", + "LabelSelectUsers": "Benutzer auswählen", "LabelSendEbookToDevice": "E-Book senden an...", "LabelSequence": "Reihenfolge", "LabelSeries": "Serien", From 107b4b83c1350401b96d3fbdf3db0b691eceef6c Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 22 Nov 2023 18:40:42 +0200 Subject: [PATCH 168/208] Add cache middleware to most /libraries get requests --- server/managers/ApiCacheManager | 59 ++++++++++++++++++--------------- server/routers/ApiRouter.js | 20 +++++------ 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/server/managers/ApiCacheManager b/server/managers/ApiCacheManager index 9d80fdb2e..882b9b61a 100644 --- a/server/managers/ApiCacheManager +++ b/server/managers/ApiCacheManager @@ -1,42 +1,47 @@ const { LRUCache } = require('lru-cache') const Logger = require('../Logger') -const { measure } = require('../utils/timing') +const Database = require('../Database') class ApiCacheManager { - constructor() { - this.options = { - max: 1000, - maxSize: 10 * 1000 * 1000, - sizeCalculation: item => item.length, - } + constructor(options = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: item => item.length }) { + this.options = options } - init() { + init(database = Database) { this.cache = new LRUCache(this.options) + let hooks = ['afterCreate', 'afterUpdate', 'afterDestroy', 'afterBulkCreate', 'afterBulkUpdate', 'afterBulkDestroy'] + hooks.forEach(hook => database.sequelize.addHook(hook, (model) => this.clear(model, hook))) + } + + clear(model, hook) { + Logger.debug(`[ApiCacheManager] ${model.constructor.name}.${hook}: Clearing cache`) + this.cache.clear() } get middleware() { return (req, res, next) => { - measure('ApiCacheManager.middleware', () => { - const key = req.originalUrl || req.url - Logger.debug(`[ApiCacheManager] Cache key: ${key}`) - Logger.debug(`[ApiCacheManager] Cache: ${this.cache} count: ${this.cache.size} size: ${this.cache.calculatedSize}`) - const cached = this.cache.get(key) - if (cached) { - Logger.debug(`[ApiCacheManager] Cache hit: ${key}`) - res.send(cached) - return + const key = { user: req.user.username, url: req.url } + const stringifiedKey = JSON.stringify(key) + Logger.debug(`[ApiCacheManager] count: ${this.cache.size} size: ${this.cache.calculatedSize}`) + Logger.debug(`[ApiCacheManager] Cache key: ${stringifiedKey}`) + const cached = this.cache.get(stringifiedKey) + if (cached) { + Logger.debug(`[ApiCacheManager] Cache hit: ${stringifiedKey}`) + res.send(cached) + return + } + res.sendResponse = res.send + res.send = (body) => { + Logger.debug(`[ApiCacheManager] Cache miss: ${stringifiedKey}`) + if (key.url.search(/^\/libraries\/.*?\/personalized/) !== -1) { + Logger.debug(`[ApiCacheManager] Caching personalized with 30 minues TTL`) + this.cache.set(stringifiedKey, body, { ttl: 30 * 60 * 1000 }) + } else { + this.cache.set(stringifiedKey, body) } - res.sendResponse = res.send - res.send = (body) => { - Logger.debug(`[ApiCacheManager] Cache miss: ${key}`) - measure('ApiCacheManager.middleware: res.send', () => { - this.cache.set(key, body) - res.sendResponse(body) - }) - } - next() - }) + res.sendResponse(body) + } + next() } } } diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 43c326287..e499b2cf3 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -68,20 +68,20 @@ class ApiRouter { this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getLibraryItems.bind(this)) this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this)) - this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this)) - this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this)) + this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getEpisodeDownloadQueue.bind(this)) + this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getAllSeriesForLibrary.bind(this)) this.router.get('/libraries/:id/series/:seriesId', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this)) - this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this)) - this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this)) - this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getUserPersonalizedShelves.bind(this)) - this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this)) - this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this)) + this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getCollectionsForLibrary.bind(this)) + this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getUserPlaylistsForLibrary.bind(this)) + this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getUserPersonalizedShelves.bind(this)) + this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getLibraryFilterData.bind(this)) + this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.search.bind(this)) this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this)) - this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this)) - this.router.get('/libraries/:id/narrators', LibraryController.middleware.bind(this), LibraryController.getNarrators.bind(this)) + this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getAuthors.bind(this)) + this.router.get('/libraries/:id/narrators', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getNarrators.bind(this)) this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.updateNarrator.bind(this)) this.router.delete('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.removeNarrator.bind(this)) - this.router.get('/libraries/:id/matchall', LibraryController.middleware.bind(this), LibraryController.matchAll.bind(this)) + this.router.get('/libraries/:id/matchall', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.matchAll.bind(this)) this.router.post('/libraries/:id/scan', LibraryController.middleware.bind(this), LibraryController.scan.bind(this)) this.router.get('/libraries/:id/recent-episodes', LibraryController.middleware.bind(this), LibraryController.getRecentEpisodes.bind(this)) this.router.get('/libraries/:id/opml', LibraryController.middleware.bind(this), LibraryController.getOPMLFile.bind(this)) From 5aeb6ade729ca071a4b8079e218ac9afa842d686 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 22 Nov 2023 19:00:11 +0200 Subject: [PATCH 169/208] Merge branch 'caching' of https://github.com/mikiher/audiobookshelf into caching --- .gitignore | 3 +- .vscode/settings.json | 3 +- client/assets/app.css | 20 + client/components/app/ConfigSideNav.vue | 5 + client/components/ui/Btn.vue | 24 +- client/pages/config.vue | 1 + client/pages/config/authentication.vue | 229 + client/pages/login.vue | 81 +- client/store/index.js | 2 +- client/strings/en-us.json | 1 + package-lock.json | 4743 ++++++++++++++++++++- package.json | 20 +- server/Auth.js | 678 ++- server/Logger.js | 2 +- server/Server.js | 69 +- server/SocketAuthority.js | 27 +- server/controllers/MiscController.js | 117 +- server/controllers/SessionController.js | 20 +- server/controllers/UserController.js | 4 +- server/finders/BookFinder.js | 158 +- server/libs/passportLocal/LICENSE | 20 + server/libs/passportLocal/index.js | 20 + server/libs/passportLocal/strategy.js | 119 + server/models/User.js | 119 +- server/objects/settings/ServerSettings.js | 133 +- server/objects/user/User.js | 9 +- server/routers/ApiRouter.js | 3 + test/server/finders/BookFinder.test.js | 344 ++ 28 files changed, 6580 insertions(+), 394 deletions(-) create mode 100644 client/pages/config/authentication.vue create mode 100644 server/libs/passportLocal/LICENSE create mode 100644 server/libs/passportLocal/index.js create mode 100644 server/libs/passportLocal/strategy.js create mode 100644 test/server/finders/BookFinder.test.js diff --git a/.gitignore b/.gitignore index 6f47029b0..9360600a5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,12 @@ /podcasts/ /media/ /metadata/ -test/ /client/.nuxt/ /client/dist/ /dist/ /deploy/ +/coverage/ +/.nyc_output/ sw.* .DS_STORE diff --git a/.vscode/settings.json b/.vscode/settings.json index 2fb8b48fb..397b9618e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,6 @@ }, "editor.formatOnSave": true, "editor.detectIndentation": true, - "editor.tabSize": 2 + "editor.tabSize": 2, + "javascript.format.semicolons": "remove" } \ No newline at end of file diff --git a/client/assets/app.css b/client/assets/app.css index b7b8499d2..1a83dc1c1 100644 --- a/client/assets/app.css +++ b/client/assets/app.css @@ -258,4 +258,24 @@ Bookshelf Label .no-bars .Vue-Toastification__container.top-right { padding-top: 8px; +} + +.abs-btn::before { + content: ''; + position: absolute; + border-radius: 6px; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0); + transition: all 0.1s ease-in-out; +} + +.abs-btn:hover:not(:disabled)::before { + background-color: rgba(255, 255, 255, 0.1); +} + +.abs-btn:disabled::before { + background-color: rgba(0, 0, 0, 0.2); } \ No newline at end of file diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index 267aabaa8..c2db07254 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -104,6 +104,11 @@ export default { id: 'config-rss-feeds', title: this.$strings.HeaderRSSFeeds, path: '/config/rss-feeds' + }, + { + id: 'config-authentication', + title: this.$strings.HeaderAuthentication, + path: '/config/authentication' } ] diff --git a/client/components/ui/Btn.vue b/client/components/ui/Btn.vue index d9b757154..7f73a956e 100644 --- a/client/components/ui/Btn.vue +++ b/client/components/ui/Btn.vue @@ -1,5 +1,5 @@ <template> - <nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList"> + <nuxt-link v-if="to" :to="to" class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList"> <slot /> <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> <svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24"> @@ -7,7 +7,7 @@ </svg> </div> </nuxt-link> - <button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click"> + <button v-else class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click"> <slot /> <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> <svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24"> @@ -72,23 +72,3 @@ export default { mounted() {} } </script> - -<style scoped> -.btn::before { - content: ''; - position: absolute; - border-radius: 6px; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(255, 255, 255, 0); - transition: all 0.1s ease-in-out; -} -.btn:hover:not(:disabled)::before { - background-color: rgba(255, 255, 255, 0.1); -} -button:disabled::before { - background-color: rgba(0, 0, 0, 0.2); -} -</style> \ No newline at end of file diff --git a/client/pages/config.vue b/client/pages/config.vue index 542b7f2cb..fdbd71508 100644 --- a/client/pages/config.vue +++ b/client/pages/config.vue @@ -57,6 +57,7 @@ export default { else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds else if (pageName === 'email') return this.$strings.HeaderEmail + else if (pageName === 'authentication') return this.$strings.HeaderAuthentication } return this.$strings.HeaderSettings } diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue new file mode 100644 index 000000000..9ea8172ab --- /dev/null +++ b/client/pages/config/authentication.vue @@ -0,0 +1,229 @@ +<template> + <div> + <app-settings-content :header-text="$strings.HeaderAuthentication"> + <div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25"> + <div class="flex items-center"> + <ui-checkbox v-model="enableLocalAuth" checkbox-bg="bg" /> + <p class="text-lg pl-4">Password Authentication</p> + </div> + </div> + <div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25"> + <div class="flex items-center"> + <ui-checkbox v-model="enableOpenIDAuth" checkbox-bg="bg" /> + <p class="text-lg pl-4">OpenID Connect Authentication</p> + </div> + + <transition name="slide"> + <div v-if="enableOpenIDAuth" class="flex flex-wrap pt-4"> + <div class="w-full flex items-center mb-2"> + <div class="flex-grow"> + <ui-text-input-with-label ref="issuerUrl" v-model="newAuthSettings.authOpenIDIssuerURL" :disabled="savingSettings" :label="'Issuer URL'" /> + </div> + <div class="w-36 mx-1 mt-[1.375rem]"> + <ui-btn class="h-[2.375rem] text-sm inline-flex items-center justify-center w-full" type="button" :padding-y="0" :padding-x="4" @click.stop="autoPopulateOIDCClick"> + <span class="material-icons text-base">auto_fix_high</span> + <span class="whitespace-nowrap break-keep pl-1">Auto-populate</span></ui-btn + > + </div> + </div> + + <ui-text-input-with-label ref="authorizationUrl" v-model="newAuthSettings.authOpenIDAuthorizationURL" :disabled="savingSettings" :label="'Authorize URL'" class="mb-2" /> + + <ui-text-input-with-label ref="tokenUrl" v-model="newAuthSettings.authOpenIDTokenURL" :disabled="savingSettings" :label="'Token URL'" class="mb-2" /> + + <ui-text-input-with-label ref="userInfoUrl" v-model="newAuthSettings.authOpenIDUserInfoURL" :disabled="savingSettings" :label="'Userinfo URL'" class="mb-2" /> + + <ui-text-input-with-label ref="jwksUrl" v-model="newAuthSettings.authOpenIDJwksURL" :disabled="savingSettings" :label="'JWKS URL'" class="mb-2" /> + + <ui-text-input-with-label ref="logoutUrl" v-model="newAuthSettings.authOpenIDLogoutURL" :disabled="savingSettings" :label="'Logout URL'" class="mb-2" /> + + <ui-text-input-with-label ref="openidClientId" v-model="newAuthSettings.authOpenIDClientID" :disabled="savingSettings" :label="'Client ID'" class="mb-2" /> + + <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" /> + + <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="'Button Text'" class="mb-2" /> + + <div class="flex items-center pt-1 mb-2"> + <div class="w-44"> + <ui-dropdown v-model="newAuthSettings.authOpenIDMatchExistingBy" small :items="matchingExistingOptions" label="Match existing users by" :disabled="savingSettings" /> + </div> + <p class="pl-4 text-sm text-gray-300 mt-5">Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider</p> + </div> + + <div class="flex items-center py-4 px-1"> + <ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" /> + <p id="auto-redirect-toggle" class="pl-4">Auto Launch</p> + <p class="pl-4 text-sm text-gray-300">Redirect to the auth provider automatically when navigating to the login page</p> + </div> + + <div class="flex items-center py-4 px-1"> + <ui-toggle-switch labeledBy="auto-register-toggle" v-model="newAuthSettings.authOpenIDAutoRegister" :disabled="savingSettings" /> + <p id="auto-register-toggle" class="pl-4">Auto Register</p> + <p class="pl-4 text-sm text-gray-300">Automatically create new users after logging in</p> + </div> + </div> + </transition> + </div> + <div class="w-full flex items-center justify-end p-4"> + <ui-btn color="success" :padding-x="8" small class="text-base" :loading="savingSettings" @click="saveSettings">{{ $strings.ButtonSave }}</ui-btn> + </div> + </app-settings-content> + </div> +</template> + +<script> +export default { + async asyncData({ store, redirect, app }) { + if (!store.getters['user/getIsAdminOrUp']) { + redirect('/') + return + } + + const authSettings = await app.$axios.$get('/api/auth-settings').catch((error) => { + console.error('Failed', error) + return null + }) + if (!authSettings) { + redirect('/config') + return + } + return { + authSettings + } + }, + data() { + return { + enableLocalAuth: false, + enableOpenIDAuth: false, + savingSettings: false, + newAuthSettings: {} + } + }, + computed: { + authMethods() { + return this.authSettings.authActiveAuthMethods || [] + }, + matchingExistingOptions() { + return [ + { + text: 'Do not match', + value: null + }, + { + text: 'Match by email', + value: 'email' + }, + { + text: 'Match by username', + value: 'username' + } + ] + } + }, + methods: { + autoPopulateOIDCClick() { + if (!this.newAuthSettings.authOpenIDIssuerURL) { + this.$toast.error('Issuer URL required') + return + } + // Remove trailing slash + let issuerUrl = this.newAuthSettings.authOpenIDIssuerURL + if (issuerUrl.endsWith('/')) issuerUrl = issuerUrl.slice(0, -1) + + // If the full config path is on the issuer url then remove it + if (issuerUrl.endsWith('/.well-known/openid-configuration')) { + issuerUrl = issuerUrl.replace('/.well-known/openid-configuration', '') + this.newAuthSettings.authOpenIDIssuerURL = this.newAuthSettings.authOpenIDIssuerURL.replace('/.well-known/openid-configuration', '') + } + + this.$axios + .$get(`/auth/openid/config?issuer=${issuerUrl}`) + .then((data) => { + if (data.issuer) this.newAuthSettings.authOpenIDIssuerURL = data.issuer + if (data.authorization_endpoint) this.newAuthSettings.authOpenIDAuthorizationURL = data.authorization_endpoint + if (data.token_endpoint) this.newAuthSettings.authOpenIDTokenURL = data.token_endpoint + if (data.userinfo_endpoint) this.newAuthSettings.authOpenIDUserInfoURL = data.userinfo_endpoint + if (data.end_session_endpoint) this.newAuthSettings.authOpenIDLogoutURL = data.end_session_endpoint + if (data.jwks_uri) this.newAuthSettings.authOpenIDJwksURL = data.jwks_uri + }) + .catch((error) => { + console.error('Failed to receive data', error) + const errorMsg = error.response?.data || 'Unknown error' + this.$toast.error(errorMsg) + }) + }, + validateOpenID() { + let isValid = true + if (!this.newAuthSettings.authOpenIDIssuerURL) { + this.$toast.error('Issuer URL required') + isValid = false + } + if (!this.newAuthSettings.authOpenIDAuthorizationURL) { + this.$toast.error('Authorize URL required') + isValid = false + } + if (!this.newAuthSettings.authOpenIDTokenURL) { + this.$toast.error('Token URL required') + isValid = false + } + if (!this.newAuthSettings.authOpenIDUserInfoURL) { + this.$toast.error('Userinfo URL required') + isValid = false + } + if (!this.newAuthSettings.authOpenIDJwksURL) { + this.$toast.error('JWKS URL required') + isValid = false + } + if (!this.newAuthSettings.authOpenIDClientID) { + this.$toast.error('Client ID required') + isValid = false + } + if (!this.newAuthSettings.authOpenIDClientSecret) { + this.$toast.error('Client Secret required') + isValid = false + } + return isValid + }, + async saveSettings() { + if (!this.enableLocalAuth && !this.enableOpenIDAuth) { + this.$toast.error('Must have at least one authentication method enabled') + return + } + + if (this.enableOpenIDAuth && !this.validateOpenID()) { + return + } + + this.newAuthSettings.authActiveAuthMethods = [] + if (this.enableLocalAuth) this.newAuthSettings.authActiveAuthMethods.push('local') + if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid') + + this.savingSettings = true + this.$axios + .$patch('/api/auth-settings', this.newAuthSettings) + .then((data) => { + this.$store.commit('setServerSettings', data.serverSettings) + this.$toast.success('Server settings updated') + }) + .catch((error) => { + console.error('Failed to update server settings', error) + this.$toast.error('Failed to update server settings') + }) + .finally(() => { + this.savingSettings = false + }) + }, + init() { + this.newAuthSettings = { + ...this.authSettings + } + this.enableLocalAuth = this.authMethods.includes('local') + this.enableOpenIDAuth = this.authMethods.includes('openid') + } + }, + mounted() { + this.init() + } +} +</script> + diff --git a/client/pages/login.vue b/client/pages/login.vue index f1e58d33a..f7579dd61 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -25,9 +25,12 @@ </div> <div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40"> <p class="text-3xl text-white text-center mb-4">{{ $strings.HeaderLogin }}</p> + <div class="w-full h-px bg-white bg-opacity-10 my-4" /> + <p v-if="error" class="text-error text-center py-2">{{ error }}</p> - <form @submit.prevent="submitForm"> + + <form v-show="login_local" @submit.prevent="submitForm"> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label> <ui-text-input v-model="username" :disabled="processing" class="mb-3 w-full" /> @@ -37,6 +40,14 @@ <ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn> </div> </form> + + <div v-if="login_local && login_openid" class="w-full h-px bg-white bg-opacity-10 my-4" /> + + <div class="w-full flex py-3"> + <a v-if="login_openid" :href="openidAuthUri" class="w-full abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center bg-primary text-white px-8 py-2 leading-none"> + {{ openIDButtonText }} + </a> + </div> </div> </div> </div> @@ -60,7 +71,10 @@ export default { }, confirmPassword: '', ConfigPath: '', - MetadataPath: '' + MetadataPath: '', + login_local: true, + login_openid: false, + authFormData: null } }, watch: { @@ -93,6 +107,12 @@ export default { computed: { user() { return this.$store.state.user.user + }, + openidAuthUri() { + return `${process.env.serverUrl}/auth/openid?callback=${location.href.split('?').shift()}` + }, + openIDButtonText() { + return this.authFormData?.authOpenIDButtonText || 'Login with OpenId' } }, methods: { @@ -162,6 +182,7 @@ export default { else this.error = 'Unknown Error' return false }) + if (authRes?.error) { this.error = authRes.error } else if (authRes) { @@ -196,28 +217,62 @@ export default { this.processing = true this.$axios .$get('/status') - .then((res) => { - this.processing = false - this.isInit = res.isInit - this.showInitScreen = !res.isInit - this.$setServerLanguageCode(res.language) + .then((data) => { + this.isInit = data.isInit + this.showInitScreen = !data.isInit + this.$setServerLanguageCode(data.language) if (this.showInitScreen) { - this.ConfigPath = res.ConfigPath || '' - this.MetadataPath = res.MetadataPath || '' + this.ConfigPath = data.ConfigPath || '' + this.MetadataPath = data.MetadataPath || '' + } else { + this.authFormData = data.authFormData + this.updateLoginVisibility(data.authMethods || []) } }) .catch((error) => { console.error('Status check failed', error) - this.processing = false this.criticalError = 'Status check failed' }) + .finally(() => { + this.processing = false + }) + }, + updateLoginVisibility(authMethods) { + if (this.$route.query?.error) { + this.error = this.$route.query.error + + // Remove error query string + const newurl = new URL(location.href) + newurl.searchParams.delete('error') + window.history.replaceState({ path: newurl.href }, '', newurl.href) + } + + if (authMethods.includes('local') || !authMethods.length) { + this.login_local = true + } else { + this.login_local = false + } + + if (authMethods.includes('openid')) { + // Auto redirect unless query string ?autoLaunch=0 + if (this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') { + window.location.href = this.openidAuthUri + } + + this.login_openid = true + } else { + this.login_openid = false + } } }, async mounted() { - if (localStorage.getItem('token')) { - var userfound = await this.checkAuth() - if (userfound) return // if valid user no need to check status + if (this.$route.query?.setToken) { + localStorage.setItem('token', this.$route.query.setToken) } + if (localStorage.getItem('token')) { + if (await this.checkAuth()) return // if valid user no need to check status + } + this.checkStatus() } } diff --git a/client/store/index.js b/client/store/index.js index 2f8201c16..ed7c35b61 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -66,7 +66,7 @@ export const getters = { export const actions = { updateServerSettings({ commit }, payload) { - var updatePayload = { + const updatePayload = { ...payload } return this.$axios.$patch('/api/settings', updatePayload).then((result) => { diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 1366c762a..6f06ca77e 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise Notification Settings", "HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudioTracks": "Audio Tracks", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Backups", "HeaderChangePassword": "Change Password", "HeaderChapters": "Chapters", diff --git a/package-lock.json b/package-lock.json index fedfddc6a..e1a5f266a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,17 @@ "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", + "cookie-parser": "^1.4.6", "express": "^4.17.1", + "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", - "lru-cache": "^10.0.2", + "lru-cache": "^10.0.3", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", + "openid-client": "^5.6.1", + "passport": "^0.6.0", + "passport-jwt": "^4.0.1", "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", @@ -26,7 +31,479 @@ "audiobookshelf": "prod.js" }, "devDependencies": { - "nodemon": "^2.0.20" + "chai": "^4.3.10", + "mocha": "^10.2.0", + "nodemon": "^2.0.20", + "nyc": "^15.1.0", + "sinon": "^17.0.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/compat-data": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", + "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", + "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.3", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.3", + "@babel/types": "^7.23.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/parser": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@gar/promisify": { @@ -35,6 +512,79 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -144,6 +694,50 @@ "node": ">=10" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -284,7 +878,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "optional": true, + "devOptional": true, "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -293,6 +887,15 @@ "node": ">=8" } }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -301,6 +904,21 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -314,11 +932,29 @@ "node": ">= 8" } }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -331,11 +967,29 @@ "node": ">=10" } }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -416,6 +1070,49 @@ "node": ">=8" } }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -465,6 +1162,21 @@ "node": ">=10" } }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -477,6 +1189,102 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -516,11 +1324,54 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "optional": true, + "devOptional": true, "engines": { "node": ">=6" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -540,6 +1391,12 @@ "node": ">= 0.8" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -569,6 +1426,12 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -577,6 +1440,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -594,6 +1477,20 @@ "node": ">= 0.10" } }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -602,6 +1499,45 @@ "ms": "2.0.0" } }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -640,6 +1576,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -696,11 +1641,25 @@ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "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==" }, + "node_modules/electron-to-chromium": { + "version": "1.4.580", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.580.tgz", + "integrity": "sha512-T5q3pjQon853xxxHUq3ZP68ZpvJHuSMY2+BZaW3QzjS4HvNuvsMmZ/+lU+nCrftre1jFZ+OSlExynXWBihnXzw==", + "dev": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -818,11 +1777,48 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "optional": true }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "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==" }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -872,6 +1868,32 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -901,6 +1923,45 @@ "node": ">= 0.8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -920,6 +1981,19 @@ } } }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -949,6 +2023,26 @@ "node": ">= 0.6" } }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -965,24 +2059,13 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/gauge": { "version": "3.0.2", @@ -1003,6 +2086,33 @@ "node": ">=10" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -1016,6 +2126,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1047,6 +2166,15 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -1088,6 +2216,37 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/htmlparser2": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", @@ -1227,7 +2386,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "optional": true, + "devOptional": true, "engines": { "node": ">=0.8.19" } @@ -1236,7 +2395,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "optional": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -1339,29 +2498,340 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true + "devOptional": true }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lru-cache": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", - "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", - "dependencies": { - "semver": "^7.3.5" - }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, "engines": { - "node": "14 || >=16.14" + "node": ">=8" } }, - "node_modules/lru-cache/node_modules/lru-cache": { + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", @@ -1372,7 +2842,12 @@ "node": ">=10" } }, - "node_modules/lru-cache/node_modules/semver": { + "node_modules/jsonwebtoken/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==" + }, + "node_modules/jsonwebtoken/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", @@ -1386,6 +2861,128 @@ "node": ">=10" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz", + "integrity": "sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==", + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -1608,6 +3205,266 @@ "node": ">=10" } }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/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==", + "dev": true + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/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==", + "dev": true + }, + "node_modules/mocha/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -1632,6 +3489,18 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1640,6 +3509,37 @@ "node": ">= 0.6" } }, + "node_modules/nise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/node-addon-api": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", @@ -1777,6 +3677,24 @@ "node": ">=10" } }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, "node_modules/node-tone": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz", @@ -1868,6 +3786,68 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1876,6 +3856,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -1884,6 +3872,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1895,6 +3891,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1903,6 +3907,73 @@ "wrappy": "1" } }, + "node_modules/openid-client": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", + "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", + "dependencies": { + "jose": "^4.15.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -1918,6 +3989,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1926,6 +4021,49 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1934,16 +4072,45 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pg-connection-string": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1956,6 +4123,30 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -2007,6 +4198,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2054,6 +4262,42 @@ "node": ">=8.10.0" } }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -2263,6 +4507,15 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -2287,6 +4540,27 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -2326,6 +4600,63 @@ "semver": "bin/semver.js" } }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -2462,6 +4793,38 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/sqlite3": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", @@ -2552,6 +4915,27 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2588,6 +4972,29 @@ "node": ">=8" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2630,6 +5037,24 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2642,6 +5067,26 @@ "node": ">= 0.6" } }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -2674,6 +5119,36 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2729,7 +5204,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "optional": true, + "devOptional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -2740,6 +5215,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -2756,11 +5237,46 @@ "@types/node": "*" } }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "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==" }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/ws": { "version": "8.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", @@ -2801,19 +5317,552 @@ "node": ">=4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs-parser/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } }, "dependencies": { + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + } + } + }, + "@babel/compat-data": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", + "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "dev": true + }, + "@babel/core": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", + "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.3", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.3", + "@babel/types": "^7.23.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "dev": true, + "requires": { + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true + }, + "@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + } + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + } + } + }, + "@babel/parser": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "dev": true + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "@mapbox/node-pre-gyp": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", @@ -2896,6 +5945,52 @@ "rimraf": "^3.0.2" } }, + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -3012,17 +6107,32 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "optional": true, + "devOptional": true, "requires": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -3033,11 +6143,26 @@ "picomatch": "^2.0.4" } }, + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, "aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -3047,11 +6172,26 @@ "readable-stream": "^3.6.0" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3119,6 +6259,29 @@ "fill-range": "^7.0.1" } }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3161,6 +6324,18 @@ } } }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -3170,6 +6345,69 @@ "get-intrinsic": "^1.0.2" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "dev": true + }, + "chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -3195,7 +6433,46 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "optional": true + "devOptional": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "color-support": { "version": "1.1.3", @@ -3210,6 +6487,12 @@ "delayed-stream": "~1.0.0" } }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3233,11 +6516,33 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" }, + "cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "requires": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + } + } + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -3252,6 +6557,17 @@ "vary": "^1" } }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3260,6 +6576,30 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3285,6 +6625,12 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, "dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -3323,11 +6669,25 @@ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "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==" }, + "electron-to-chromium": { + "version": "1.4.580", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.580.tgz", + "integrity": "sha512-T5q3pjQon853xxxHUq3ZP68ZpvJHuSMY2+BZaW3QzjS4HvNuvsMmZ/+lU+nCrftre1jFZ+OSlExynXWBihnXzw==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3417,11 +6777,35 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "optional": true }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "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==" }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3465,6 +6849,28 @@ "vary": "~1.1.2" } }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + } + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3488,11 +6894,48 @@ "unpipe": "~1.0.0" } }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -3513,6 +6956,12 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -3526,17 +6975,10 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "gauge": { "version": "3.0.2", @@ -3554,6 +6996,24 @@ "wide-align": "^1.1.2" } }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, "get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -3564,6 +7024,12 @@ "has-symbols": "^1.0.3" } }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3586,6 +7052,12 @@ "is-glob": "^4.0.1" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -3615,6 +7087,28 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "htmlparser2": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", @@ -3723,13 +7217,13 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "optional": true + "devOptional": true }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "optional": true + "devOptional": true }, "infer-owner": { "version": "1.0.4", @@ -3808,23 +7302,249 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true + "devOptional": true }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true }, - "lru-cache": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", - "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", + "istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, "requires": { - "semver": "^7.3.5" + "append-transform": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "dependencies": { + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" }, "dependencies": { "lru-cache": { @@ -3835,6 +7555,11 @@ "yallist": "^4.0.0" } }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -3845,6 +7570,116 @@ } } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "requires": { + "get-func-name": "^2.0.1" + } + }, + "lru-cache": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz", + "integrity": "sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -4006,6 +7841,201 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, + "mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + } + } + }, "moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -4024,11 +8054,50 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true + }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, + "nise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "node-addon-api": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", @@ -4127,6 +8196,21 @@ } } }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, "node-tone": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz", @@ -4198,16 +8282,78 @@ "set-blocking": "^2.0.0" } }, + "nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + } + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + }, "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" }, + "oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==" + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4216,6 +8362,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4224,6 +8375,56 @@ "wrappy": "1" } }, + "openid-client": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", + "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", + "requires": { + "jose": "^4.15.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, "p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -4233,32 +8434,121 @@ "aggregate-error": "^3.0.0" } }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "requires": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "pg-connection-string": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -4298,6 +8588,20 @@ "side-channel": "^1.0.4" } }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4333,6 +8637,33 @@ "picomatch": "^2.2.1" } }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -4459,6 +8790,15 @@ "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==" }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -4480,6 +8820,21 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -4512,6 +8867,52 @@ } } }, + "sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -4613,6 +9014,32 @@ } } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "sqlite3": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", @@ -4679,6 +9106,18 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4708,6 +9147,23 @@ } } }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4741,6 +9197,18 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -4750,6 +9218,23 @@ "mime-types": "~2.1.24" } }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -4779,6 +9264,16 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4822,11 +9317,17 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "optional": true, + "devOptional": true, "requires": { "isexe": "^2.0.0" } }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -4843,11 +9344,40 @@ "@types/node": "*" } }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "ws": { "version": "8.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", @@ -4868,10 +9398,93 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + } + } + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/package.json b/package.json index dafd8907b..477f62afd 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local", "docker-arm64-local": "docker buildx build --platform linux/arm64 --load . -t advplyr/audiobookshelf-arm64-local", "docker-armv7-local": "docker buildx build --platform linux/arm/v7 --load . -t advplyr/audiobookshelf-armv7-local", - "deploy-linux": "node deploy/linux" + "deploy-linux": "node deploy/linux", + "test": "mocha", + "coverage": "nyc mocha" }, "bin": "prod.js", "pkg": { @@ -28,16 +30,24 @@ "server/**/*.js" ] }, + "mocha": { + "recursive": true + }, "author": "advplyr", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", + "cookie-parser": "^1.4.6", "express": "^4.17.1", + "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", - "lru-cache": "^10.0.2", + "lru-cache": "^10.0.3", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", + "openid-client": "^5.6.1", + "passport": "^0.6.0", + "passport-jwt": "^4.0.1", "sequelize": "^6.32.1", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", @@ -45,6 +55,10 @@ "xml2js": "^0.5.0" }, "devDependencies": { - "nodemon": "^2.0.20" + "chai": "^4.3.10", + "mocha": "^10.2.0", + "nodemon": "^2.0.20", + "nyc": "^15.1.0", + "sinon": "^17.0.1" } } diff --git a/server/Auth.js b/server/Auth.js index 6c7b98914..e2053fa56 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,32 +1,466 @@ +const axios = require('axios') +const passport = require('passport') const bcrypt = require('./libs/bcryptjs') const jwt = require('./libs/jsonwebtoken') -const requestIp = require('./libs/requestIp') -const Logger = require('./Logger') +const LocalStrategy = require('./libs/passportLocal') +const JwtStrategy = require('passport-jwt').Strategy +const ExtractJwt = require('passport-jwt').ExtractJwt +const OpenIDClient = require('openid-client') const Database = require('./Database') +const Logger = require('./Logger') +/** + * @class Class for handling all the authentication related functionality. + */ class Auth { - constructor() { } - cors(req, res, next) { - res.header('Access-Control-Allow-Origin', '*') - res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS') - res.header('Access-Control-Allow-Headers', '*') - // TODO: Make sure allowing all headers is not a security concern. It is required for adding custom headers for SSO - // res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Range, Authorization") - res.header('Access-Control-Allow-Credentials', true) - if (req.method === 'OPTIONS') { - res.sendStatus(200) + constructor() { + } + + /** + * Inializes all passportjs strategies and other passportjs ralated initialization. + */ + async initPassportJs() { + // Check if we should load the local strategy (username + password login) + if (global.ServerSettings.authActiveAuthMethods.includes("local")) { + this.initAuthStrategyPassword() + } + + // Check if we should load the openid strategy + if (global.ServerSettings.authActiveAuthMethods.includes("openid")) { + this.initAuthStrategyOpenID() + } + + // Load the JwtStrategy (always) -> for bearer token auth + passport.use(new JwtStrategy({ + jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]), + secretOrKey: Database.serverSettings.tokenSecret + }, this.jwtAuthCheck.bind(this))) + + // define how to seralize a user (to be put into the session) + passport.serializeUser(function (user, cb) { + process.nextTick(function () { + // only store id to session + return cb(null, JSON.stringify({ + id: user.id, + })) + }) + }) + + // define how to deseralize a user (use the ID to get it from the database) + passport.deserializeUser((function (user, cb) { + process.nextTick((async function () { + const parsedUserInfo = JSON.parse(user) + // load the user by ID that is stored in the session + const dbUser = await Database.userModel.getUserById(parsedUserInfo.id) + return cb(null, dbUser) + }).bind(this)) + }).bind(this)) + } + + /** + * Passport use LocalStrategy + */ + initAuthStrategyPassword() { + passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this))) + } + + /** + * Passport use OpenIDClient.Strategy + */ + initAuthStrategyOpenID() { + if (!Database.serverSettings.isOpenIDAuthSettingsValid) { + Logger.error(`[Auth] Cannot init openid auth strategy - invalid settings`) + return + } + + const openIdIssuerClient = new OpenIDClient.Issuer({ + issuer: global.ServerSettings.authOpenIDIssuerURL, + authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL, + token_endpoint: global.ServerSettings.authOpenIDTokenURL, + userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL, + jwks_uri: global.ServerSettings.authOpenIDJwksURL + }).Client + const openIdClient = new openIdIssuerClient({ + client_id: global.ServerSettings.authOpenIDClientID, + client_secret: global.ServerSettings.authOpenIDClientSecret + }) + passport.use('openid-client', new OpenIDClient.Strategy({ + client: openIdClient, + params: { + redirect_uri: '/auth/openid/callback', + scope: 'openid profile email' + } + }, async (tokenset, userinfo, done) => { + Logger.debug(`[Auth] openid callback userinfo=`, userinfo) + + let failureMessage = 'Unauthorized' + if (!userinfo.sub) { + Logger.error(`[Auth] openid callback invalid userinfo, no sub`) + return done(null, null, failureMessage) + } + + // First check for matching user by sub + let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) + if (!user) { + // Optionally match existing by email or username based on server setting "authOpenIDMatchExistingBy" + if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) { + Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`) + user = await Database.userModel.getUserByEmail(userinfo.email) + // Check that user is not already matched + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) + // TODO: Message isn't actually returned to the user yet. Need to override the passport authenticated callback + failureMessage = 'A matching user was found but is already matched with another user from your auth provider' + user = null + } + } else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) { + Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`) + user = await Database.userModel.getUserByUsername(userinfo.preferred_username) + // Check that user is not already matched + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) + // TODO: Message isn't actually returned to the user yet. Need to override the passport authenticated callback + failureMessage = 'A matching user was found but is already matched with another user from your auth provider' + user = null + } + } + + // If existing user was matched and isActive then save sub to user + if (user?.isActive) { + Logger.info(`[Auth] openid: New user found matching existing user "${user.username}"`) + user.authOpenIDSub = userinfo.sub + await Database.userModel.updateFromOld(user) + } else if (user && !user.isActive) { + Logger.warn(`[Auth] openid: New user found matching existing user "${user.username}" but that user is deactivated`) + } + + // Optionally auto register the user + if (!user && Database.serverSettings.authOpenIDAutoRegister) { + Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo) + user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this) + } + } + + if (!user?.isActive) { + if (user && !user.isActive) { + failureMessage = 'Unauthorized' + } + // deny login + done(null, null, failureMessage) + return + } + + // permit login + return done(null, user) + })) + } + + /** + * Unuse strategy + * + * @param {string} name + */ + unuseAuthStrategy(name) { + passport.unuse(name) + } + + /** + * Use strategy + * + * @param {string} name + */ + useAuthStrategy(name) { + if (name === 'openid') { + this.initAuthStrategyOpenID() + } else if (name === 'local') { + this.initAuthStrategyPassword() } else { - next() + Logger.error('[Auth] Invalid auth strategy ' + name) } } + /** + * Stores the client's choice how the login callback should happen in temp cookies + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + paramsToCookies(req, res) { + if (req.query.isRest?.toLowerCase() == 'true') { + // store the isRest flag to the is_rest cookie + res.cookie('is_rest', req.query.isRest.toLowerCase(), { + maxAge: 120000, // 2 min + httpOnly: true + }) + } else { + // no isRest-flag set -> set is_rest cookie to false + res.cookie('is_rest', 'false', { + maxAge: 120000, // 2 min + httpOnly: true + }) + + // persist state if passed in + if (req.query.state) { + res.cookie('auth_state', req.query.state, { + maxAge: 120000, // 2 min + httpOnly: true + }) + } + + const callback = req.query.redirect_uri || req.query.callback + + // check if we are missing a callback parameter - we need one if isRest=false + if (!callback) { + res.status(400).send({ + message: 'No callback parameter' + }) + return + } + // store the callback url to the auth_cb cookie + res.cookie('auth_cb', callback, { + maxAge: 120000, // 2 min + httpOnly: true + }) + } + } + + /** + * Informs the client in the right mode about a successfull login and the token + * (clients choise is restored from cookies). + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async handleLoginSuccessBasedOnCookie(req, res) { + // get userLogin json (information about the user, server and the session) + const data_json = await this.getUserLoginResponsePayload(req.user) + + if (req.cookies.is_rest === 'true') { + // REST request - send data + res.json(data_json) + } else { + // UI request -> check if we have a callback url + // TODO: do we want to somehow limit the values for auth_cb? + if (req.cookies.auth_cb) { + let stateQuery = req.cookies.auth_state ? `&state=${req.cookies.auth_state}` : '' + // UI request -> redirect to auth_cb url and send the jwt token as parameter + res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}${stateQuery}`) + } else { + res.status(400).send('No callback or already expired') + } + } + } + + /** + * Creates all (express) routes required for authentication. + * + * @param {import('express').Router} router + */ + async initAuthRoutes(router) { + // Local strategy login route (takes username and password) + router.post('/login', passport.authenticate('local'), async (req, res) => { + // return the user login response json if the login was successfull + res.json(await this.getUserLoginResponsePayload(req.user)) + }) + + // openid strategy login route (this redirects to the configured openid login provider) + router.get('/auth/openid', (req, res, next) => { + try { + // helper function from openid-client + function pick(object, ...paths) { + const obj = {} + for (const path of paths) { + if (object[path] !== undefined) { + obj[path] = object[path] + } + } + return obj + } + + // Get the OIDC client from the strategy + // We need to call the client manually, because the strategy does not support forwarding the code challenge + // for API or mobile clients + const oidcStrategy = passport._strategy('openid-client') + const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http' + oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString() + Logger.debug(`[Auth] Set oidc redirect_uri=${oidcStrategy._params.redirect_uri}`) + const client = oidcStrategy._client + const sessionKey = oidcStrategy._key + + let code_challenge + let code_challenge_method + + // If code_challenge is provided, expect that code_verifier will be handled by the client (mobile app) + // The web frontend of ABS does not need to do a PKCE itself, because it never handles the "code" of the oauth flow + // and as such will not send a code challenge, we will generate then one + if (req.query.code_challenge) { + code_challenge = req.query.code_challenge + code_challenge_method = req.query.code_challenge_method || 'S256' + + if (!['S256', 'plain'].includes(code_challenge_method)) { + return res.status(400).send('Invalid code_challenge_method') + } + } else { + // If no code_challenge is provided, assume a web application flow and generate one + const code_verifier = OpenIDClient.generators.codeVerifier() + code_challenge = OpenIDClient.generators.codeChallenge(code_verifier) + code_challenge_method = 'S256' + + // Store the code_verifier in the session for later use in the token exchange + req.session[sessionKey] = { ...req.session[sessionKey], code_verifier } + } + + const params = { + state: OpenIDClient.generators.random(), + // Other params by the passport strategy + ...oidcStrategy._params + } + + if (!params.nonce && params.response_type.includes('id_token')) { + params.nonce = OpenIDClient.generators.random() + } + + req.session[sessionKey] = { + ...req.session[sessionKey], + ...pick(params, 'nonce', 'state', 'max_age', 'response_type'), + mobile: req.query.isRest?.toLowerCase() === 'true' // Used in the abs callback later + } + + // Now get the URL to direct to + const authorizationUrl = client.authorizationUrl({ + ...params, + scope: 'openid profile email', + response_type: 'code', + code_challenge, + code_challenge_method, + }) + + // params (isRest, callback) to a cookie that will be send to the client + this.paramsToCookies(req, res) + + // Redirect the user agent (browser) to the authorization URL + res.redirect(authorizationUrl) + } catch (error) { + Logger.error(`[Auth] Error in /auth/openid route: ${error}`) + res.status(500).send('Internal Server Error') + } + }) + + // openid strategy callback route (this receives the token from the configured openid login provider) + router.get('/auth/openid/callback', (req, res, next) => { + const oidcStrategy = passport._strategy('openid-client') + const sessionKey = oidcStrategy._key + + if (!req.session[sessionKey]) { + return res.status(400).send('No session') + } + + // If the client sends us a code_verifier, we will tell passport to use this to send this in the token request + // The code_verifier will be validated by the oauth2 provider by comparing it to the code_challenge in the first request + // Crucial for API/Mobile clients + if (req.query.code_verifier) { + req.session[sessionKey].code_verifier = req.query.code_verifier + } + + // While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request + // We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided + if (req.session[sessionKey].mobile) { + return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' })(req, res, next) + } else { + return passport.authenticate('openid-client', { failureRedirect: '/login?error=Unauthorized&autoLaunch=0' })(req, res, next) + } + }, + // on a successfull login: read the cookies and react like the client requested (callback or json) + this.handleLoginSuccessBasedOnCookie.bind(this)) + + /** + * Used to auto-populate the openid URLs in config/authentication + */ + router.get('/auth/openid/config', async (req, res) => { + if (!req.query.issuer) { + return res.status(400).send('Invalid request. Query param \'issuer\' is required') + } + let issuerUrl = req.query.issuer + if (issuerUrl.endsWith('/')) issuerUrl = issuerUrl.slice(0, -1) + + const configUrl = `${issuerUrl}/.well-known/openid-configuration` + axios.get(configUrl).then(({ data }) => { + res.json({ + issuer: data.issuer, + authorization_endpoint: data.authorization_endpoint, + token_endpoint: data.token_endpoint, + userinfo_endpoint: data.userinfo_endpoint, + end_session_endpoint: data.end_session_endpoint, + jwks_uri: data.jwks_uri + }) + }).catch((error) => { + Logger.error(`[Auth] Failed to get openid configuration at "${configUrl}"`, error) + res.status(error.statusCode || 400).send(`${error.code || 'UNKNOWN'}: Failed to get openid configuration`) + }) + }) + + // Logout route + router.post('/logout', (req, res) => { + // TODO: invalidate possible JWTs + req.logout((err) => { + if (err) { + res.sendStatus(500) + } else { + res.sendStatus(200) + } + }) + }) + } + + /** + * middleware to use in express to only allow authenticated users. + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next + */ + isAuthenticated(req, res, next) { + // check if session cookie says that we are authenticated + if (req.isAuthenticated()) { + next() + } else { + // try JWT to authenticate + passport.authenticate("jwt")(req, res, next) + } + } + + /** + * Function to generate a jwt token for a given user + * + * @param {{ id:string, username:string }} user + * @returns {string} token + */ + generateAccessToken(user) { + return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret) + } + + /** + * Function to validate a jwt token for a given user + * + * @param {string} token + * @returns {Object} tokens data + */ + static validateAccessToken(token) { + try { + return jwt.verify(token, global.ServerSettings.tokenSecret) + } + catch (err) { + return null + } + } + + /** + * Generate a token which is used to encrpt/protect the jwts. + */ async initTokenSecret() { if (process.env.TOKEN_SECRET) { // User can supply their own token secret - Logger.debug(`[Auth] Setting token secret - using user passed in TOKEN_SECRET env var`) Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET } else { - Logger.debug(`[Auth] Setting token secret - using random bytes`) Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') } await Database.updateServerSettings() @@ -35,47 +469,79 @@ class Auth { const users = await Database.userModel.getOldUsers() if (users.length) { for (const user of users) { - user.token = await this.generateAccessToken({ userId: user.id, username: user.username }) - Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`) + user.token = await this.generateAccessToken(user) } await Database.updateBulkUsers(users) } } - async authMiddleware(req, res, next) { - var token = null + /** + * Checks if the user in the validated jwt_payload really exists and is active. + * @param {Object} jwt_payload + * @param {function} done + */ + async jwtAuthCheck(jwt_payload, done) { + // load user by id from the jwt token + const user = await Database.userModel.getUserByIdOrOldId(jwt_payload.userId) - // If using a get request, the token can be passed as a query string - if (req.method === 'GET' && req.query && req.query.token) { - token = req.query.token - } else { - const authHeader = req.headers['authorization'] - token = authHeader && authHeader.split(' ')[1] + if (!user?.isActive) { + // deny login + done(null, null) + return } - - if (token == null) { - Logger.error('Api called without a token', req.path) - return res.sendStatus(401) - } - - const user = await this.verifyToken(token) - if (!user) { - Logger.error('Verify Token User Not Found', token) - return res.sendStatus(404) - } - if (!user.isActive) { - Logger.error('Verify Token User is disabled', token, user.username) - return res.sendStatus(403) - } - req.user = user - next() + // approve login + done(null, user) + return } + /** + * Checks if a username and password tuple is valid and the user active. + * @param {string} username + * @param {string} password + * @param {function} done + */ + async localAuthCheckUserPw(username, password, done) { + // Load the user given it's username + const user = await Database.userModel.getUserByUsername(username.toLowerCase()) + + if (!user || !user.isActive) { + done(null, null) + return + } + + // Check passwordless root user + if (user.type === 'root' && (!user.pash || user.pash === '')) { + if (password) { + // deny login + done(null, null) + return + } + // approve login + done(null, user) + return + } + + // Check password match + const compare = await bcrypt.compare(password, user.pash) + if (compare) { + // approve login + done(null, user) + return + } + // deny login + done(null, null) + return + } + + /** + * Hashes a password with bcrypt. + * @param {string} password + * @returns {string} hash + */ hashPass(password) { return new Promise((resolve) => { bcrypt.hash(password, 8, (err, hash) => { if (err) { - Logger.error('Hash failed', err) resolve(null) } else { resolve(hash) @@ -84,36 +550,11 @@ class Auth { }) } - generateAccessToken(payload) { - return jwt.sign(payload, Database.serverSettings.tokenSecret) - } - - authenticateUser(token) { - return this.verifyToken(token) - } - - verifyToken(token) { - return new Promise((resolve) => { - jwt.verify(token, Database.serverSettings.tokenSecret, async (err, payload) => { - if (!payload || err) { - Logger.error('JWT Verify Token Failed', err) - return resolve(null) - } - - const user = await Database.userModel.getUserByIdOrOldId(payload.userId) - if (user && user.username === payload.username) { - resolve(user) - } else { - resolve(null) - } - }) - }) - } - /** - * Payload returned to a user after successful login - * @param {oldUser} user - * @returns {object} + * Return the login info payload for a user + * + * @param {Object} user + * @returns {Promise<Object>} jsonPayload */ async getUserLoginResponsePayload(user) { const libraryIds = await Database.libraryModel.getAllLibraryIds() @@ -125,97 +566,6 @@ class Auth { Source: global.Source } } - - async login(req, res) { - const ipAddress = requestIp.getClientIp(req) - const username = (req.body.username || '').toLowerCase() - const password = req.body.password || '' - - const user = await Database.userModel.getUserByUsername(username) - - if (!user?.isActive) { - Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) - if (req.rateLimit.remaining <= 2) { - Logger.error(`[Auth] Failed login attempt for username ${username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`) - return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`) - } - return res.status(401).send('Invalid user or password') - } - - // Check passwordless root user - if (user.type === 'root' && (!user.pash || user.pash === '')) { - if (password) { - return res.status(401).send('Invalid root password (hint: there is none)') - } else { - Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`) - const userLoginResponsePayload = await this.getUserLoginResponsePayload(user) - return res.json(userLoginResponsePayload) - } - } - - // Check password match - const compare = await bcrypt.compare(password, user.pash) - if (compare) { - Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`) - const userLoginResponsePayload = await this.getUserLoginResponsePayload(user) - res.json(userLoginResponsePayload) - } else { - Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) - if (req.rateLimit.remaining <= 2) { - Logger.error(`[Auth] Failed login attempt for user ${user.username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`) - return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`) - } - return res.status(401).send('Invalid user or password') - } - } - - comparePassword(password, user) { - if (user.type === 'root' && !password && !user.pash) return true - if (!password || !user.pash) return false - return bcrypt.compare(password, user.pash) - } - - async userChangePassword(req, res) { - var { password, newPassword } = req.body - newPassword = newPassword || '' - const matchingUser = await Database.userModel.getUserById(req.user.id) - - // Only root can have an empty password - if (matchingUser.type !== 'root' && !newPassword) { - return res.json({ - error: 'Invalid new password - Only root can have an empty password' - }) - } - - const compare = await this.comparePassword(password, matchingUser) - if (!compare) { - return res.json({ - error: 'Invalid password' - }) - } - - let pw = '' - if (newPassword) { - pw = await this.hashPass(newPassword) - if (!pw) { - return res.json({ - error: 'Hash failed' - }) - } - } - - matchingUser.pash = pw - - const success = await Database.updateUser(matchingUser) - if (success) { - res.json({ - success: true - }) - } else { - res.json({ - error: 'Unknown error' - }) - } - } } + module.exports = Auth \ No newline at end of file diff --git a/server/Logger.js b/server/Logger.js index 19e657b41..5eb33a244 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -11,7 +11,7 @@ class Logger { } get timestamp() { - return date.format(new Date(), 'YYYY-MM-DD HH:mm:ss') + return date.format(new Date(), 'YYYY-MM-DD HH:mm:ss.SSS') } get levelString() { diff --git a/server/Server.js b/server/Server.js index f90e9754a..69ccd2570 100644 --- a/server/Server.js +++ b/server/Server.js @@ -5,6 +5,7 @@ const http = require('http') const fs = require('./libs/fsExtra') const fileUpload = require('./libs/expressFileupload') const rateLimit = require('./libs/expressRateLimit') +const cookieParser = require("cookie-parser") const { version } = require('../package.json') @@ -35,6 +36,11 @@ const ApiCacheManager = require('./managers/ApiCacheManager') const LibraryScanner = require('./scanner/LibraryScanner') const { measureMiddleware } = require('./utils/timing') +//Import the main Passport and Express-Session library +const passport = require('passport') +const expressSession = require('express-session') + + class Server { constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { this.Port = PORT @@ -82,7 +88,8 @@ class Server { } authMiddleware(req, res, next) { - this.auth.authMiddleware(req, res, next) + // ask passportjs if the current request is authenticated + this.auth.isAuthenticated(req, res, next) } cancelLibraryScan(libraryId) { @@ -128,6 +135,50 @@ class Server { await this.init() const app = express() + + /** + * @temporary + * This is necessary for the ebook API endpoint in the mobile apps + * The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests + * so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint + * @see https://ionicframework.com/docs/troubleshooting/cors + */ + app.use((req, res, next) => { + if (req.path.match(/\/api\/items\/([a-z0-9-]{36})\/ebook(\/[0-9]+)?/)) { + const allowedOrigins = ['capacitor://localhost', 'http://localhost'] + if (allowedOrigins.some(o => o === req.get('origin'))) { + res.header('Access-Control-Allow-Origin', req.get('origin')) + res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS') + res.header('Access-Control-Allow-Headers', '*') + res.header('Access-Control-Allow-Credentials', true) + if (req.method === 'OPTIONS') { + return res.sendStatus(200) + } + } + } + + next() + }) + + // parse cookies in requests + app.use(cookieParser()) + // enable express-session + app.use(expressSession({ + secret: global.ServerSettings.tokenSecret, + resave: false, + saveUninitialized: false, + cookie: { + // also send the cookie if were are not on https (not every use has https) + secure: false + }, + })) + // init passport.js + app.use(passport.initialize()) + // register passport in express-session + app.use(passport.session()) + // config passport.js + await this.auth.initPassportJs() + const router = express.Router() app.use(global.RouterBasePath, router) app.disable('x-powered-by') @@ -135,14 +186,13 @@ class Server { this.server = http.createServer(app) router.use(measureMiddleware) - router.use(this.auth.cors) router.use(fileUpload({ defCharset: 'utf8', defParamCharset: 'utf8', useTempFiles: true, tempFileDir: Path.join(global.MetadataPath, 'tmp') })) - router.use(express.urlencoded({ extended: true, limit: "5mb" })); + router.use(express.urlencoded({ extended: true, limit: "5mb" })) router.use(express.json({ limit: "5mb" })) // Static path to generated nuxt @@ -168,6 +218,9 @@ class Server { this.rssFeedManager.getFeedItem(req, res) }) + // Auth routes + await this.auth.initAuthRoutes(router) + // Client dynamic routes const dyanimicRoutes = [ '/item/:id', @@ -191,8 +244,8 @@ class Server { ] dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html')))) - router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res)) - router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) + // router.post('/login', passport.authenticate('local', this.auth.login), this.auth.loginResult.bind(this)) + // router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) router.post('/init', (req, res) => { if (Database.hasRootUser) { Logger.error(`[Server] attempt to init server when server already has a root user`) @@ -204,8 +257,12 @@ class Server { // status check for client to see if server has been initialized // server has been initialized if a root user exists const payload = { + app: 'audiobookshelf', + serverVersion: version, isInit: Database.hasRootUser, - language: Database.serverSettings.language + language: Database.serverSettings.language, + authMethods: Database.serverSettings.authActiveAuthMethods, + authFormData: Database.serverSettings.authFormData } if (!payload.isInit) { payload.ConfigPath = global.ConfigPath diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index ea84e7df9..310121077 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -1,6 +1,7 @@ const SocketIO = require('socket.io') const Logger = require('./Logger') const Database = require('./Database') +const Auth = require('./Auth') class SocketAuthority { constructor() { @@ -81,6 +82,7 @@ class SocketAuthority { methods: ["GET", "POST"] } }) + this.io.on('connection', (socket) => { this.clients[socket.id] = { id: socket.id, @@ -144,14 +146,31 @@ class SocketAuthority { }) } - // When setting up a socket connection the user needs to be associated with a socket id - // for this the client will send a 'auth' event that includes the users API token + /** + * When setting up a socket connection the user needs to be associated with a socket id + * for this the client will send a 'auth' event that includes the users API token + * + * @param {SocketIO.Socket} socket + * @param {string} token JWT + */ async authenticateSocket(socket, token) { - const user = await this.Server.auth.authenticateUser(token) - if (!user) { + // we don't use passport to authenticate the jwt we get over the socket connection. + // it's easier to directly verify/decode it. + const token_data = Auth.validateAccessToken(token) + + if (!token_data?.userId) { + // Token invalid Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } + // get the user via the id from the decoded jwt. + const user = await Database.userModel.getUserByIdOrOldId(token_data.userId) + if (!user) { + // user not found + Logger.error('Cannot validate socket - invalid token') + return socket.emit('invalid_token') + } + const client = this.clients[socket.id] if (!client) { Logger.error(`[SocketAuthority] Socket for user ${user.username} has no client`) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index f4f1703d3..267db5c8e 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -119,8 +119,9 @@ class MiscController { /** * PATCH: /api/settings * Update server settings - * @param {*} req - * @param {*} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async updateServerSettings(req, res) { if (!req.user.isAdminOrUp) { @@ -128,7 +129,7 @@ class MiscController { return res.sendStatus(403) } const settingsUpdate = req.body - if (!settingsUpdate || !isObject(settingsUpdate)) { + if (!isObject(settingsUpdate)) { return res.status(400).send('Invalid settings update object') } @@ -248,8 +249,8 @@ class MiscController { * POST: /api/authorize * Used to authorize an API token * - * @param {*} req - * @param {*} res + * @param {import('express').Request} req + * @param {import('express').Response} res */ async authorize(req, res) { if (!req.user) { @@ -555,10 +556,10 @@ class MiscController { switch (type) { case 'add': this.watcher.onFileAdded(libraryId, path) - break; + break case 'unlink': this.watcher.onFileRemoved(libraryId, path) - break; + break case 'rename': const oldPath = req.body.oldPath if (!oldPath) { @@ -566,7 +567,7 @@ class MiscController { return res.sendStatus(400) } this.watcher.onFileRename(libraryId, oldPath, path) - break; + break default: Logger.error(`[MiscController] Invalid type for updateWatchedPath. type: "${type}"`) return res.sendStatus(400) @@ -589,5 +590,105 @@ class MiscController { res.status(400).send(error.message) } } + + /** + * GET: api/auth-settings (admin only) + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + getAuthSettings(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get auth settings`) + return res.sendStatus(403) + } + return res.json(Database.serverSettings.authenticationSettings) + } + + /** + * PATCH: api/auth-settings + * @this import('../routers/ApiRouter') + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async updateAuthSettings(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to update auth settings`) + return res.sendStatus(403) + } + + const settingsUpdate = req.body + if (!isObject(settingsUpdate)) { + return res.status(400).send('Invalid auth settings update object') + } + + let hasUpdates = false + + const currentAuthenticationSettings = Database.serverSettings.authenticationSettings + const originalAuthMethods = [...currentAuthenticationSettings.authActiveAuthMethods] + + // TODO: Better validation of auth settings once auth settings are separated from server settings + for (const key in currentAuthenticationSettings) { + if (settingsUpdate[key] === undefined) continue + + if (key === 'authActiveAuthMethods') { + let updatedAuthMethods = settingsUpdate[key]?.filter?.((authMeth) => Database.serverSettings.supportedAuthMethods.includes(authMeth)) + if (Array.isArray(updatedAuthMethods) && updatedAuthMethods.length) { + updatedAuthMethods.sort() + currentAuthenticationSettings[key].sort() + if (updatedAuthMethods.join() !== currentAuthenticationSettings[key].join()) { + Logger.debug(`[MiscController] Updating auth settings key "authActiveAuthMethods" from "${currentAuthenticationSettings[key].join()}" to "${updatedAuthMethods.join()}"`) + Database.serverSettings[key] = updatedAuthMethods + hasUpdates = true + } + } else { + Logger.warn(`[MiscController] Invalid value for authActiveAuthMethods`) + } + } else { + const updatedValueType = typeof settingsUpdate[key] + if (['authOpenIDAutoLaunch', 'authOpenIDAutoRegister'].includes(key)) { + if (updatedValueType !== 'boolean') { + Logger.warn(`[MiscController] Invalid value for ${key}. Expected boolean`) + continue + } + } else if (settingsUpdate[key] !== null && updatedValueType !== 'string') { + Logger.warn(`[MiscController] Invalid value for ${key}. Expected string or null`) + continue + } + let updatedValue = settingsUpdate[key] + if (updatedValue === '') updatedValue = null + let currentValue = currentAuthenticationSettings[key] + if (currentValue === '') currentValue = null + + if (updatedValue !== currentValue) { + Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${currentValue}" to "${updatedValue}"`) + Database.serverSettings[key] = updatedValue + hasUpdates = true + } + } + } + + if (hasUpdates) { + await Database.updateServerSettings() + + // Use/unuse auth methods + Database.serverSettings.supportedAuthMethods.forEach((authMethod) => { + if (originalAuthMethods.includes(authMethod) && !Database.serverSettings.authActiveAuthMethods.includes(authMethod)) { + // Auth method has been removed + Logger.info(`[MiscController] Disabling active auth method "${authMethod}"`) + this.auth.unuseAuthStrategy(authMethod) + } else if (!originalAuthMethods.includes(authMethod) && Database.serverSettings.authActiveAuthMethods.includes(authMethod)) { + // Auth method has been added + Logger.info(`[MiscController] Enabling active auth method "${authMethod}"`) + this.auth.useAuthStrategy(authMethod) + } + }) + } + + res.json({ + serverSettings: Database.serverSettings.toJSONForBrowser() + }) + } } module.exports = new MiscController() \ No newline at end of file diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 85baeb273..884f0cd69 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -6,7 +6,7 @@ class SessionController { constructor() { } async findOne(req, res) { - return res.json(req.session) + return res.json(req.playbackSession) } async getAllWithUserData(req, res) { @@ -63,32 +63,32 @@ class SessionController { } async getOpenSession(req, res) { - const libraryItem = await Database.libraryItemModel.getOldById(req.session.libraryItemId) - const sessionForClient = req.session.toJSONForClient(libraryItem) + const libraryItem = await Database.libraryItemModel.getOldById(req.playbackSession.libraryItemId) + const sessionForClient = req.playbackSession.toJSONForClient(libraryItem) res.json(sessionForClient) } // POST: api/session/:id/sync sync(req, res) { - this.playbackSessionManager.syncSessionRequest(req.user, req.session, req.body, res) + this.playbackSessionManager.syncSessionRequest(req.user, req.playbackSession, req.body, res) } // POST: api/session/:id/close close(req, res) { let syncData = req.body if (syncData && !Object.keys(syncData).length) syncData = null - this.playbackSessionManager.closeSessionRequest(req.user, req.session, syncData, res) + this.playbackSessionManager.closeSessionRequest(req.user, req.playbackSession, syncData, res) } // DELETE: api/session/:id async delete(req, res) { // if session is open then remove it - const openSession = this.playbackSessionManager.getSession(req.session.id) + const openSession = this.playbackSessionManager.getSession(req.playbackSession.id) if (openSession) { - await this.playbackSessionManager.removeSession(req.session.id) + await this.playbackSessionManager.removeSession(req.playbackSession.id) } - await Database.removePlaybackSession(req.session.id) + await Database.removePlaybackSession(req.playbackSession.id) res.sendStatus(200) } @@ -111,7 +111,7 @@ class SessionController { return res.sendStatus(404) } - req.session = playbackSession + req.playbackSession = playbackSession next() } @@ -130,7 +130,7 @@ class SessionController { return res.sendStatus(403) } - req.session = playbackSession + req.playbackSession = playbackSession next() } } diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 2695a7a0f..86d2c78e5 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -100,7 +100,7 @@ class UserController { account.id = uuidv4() account.pash = await this.auth.hashPass(account.password) delete account.password - account.token = await this.auth.generateAccessToken({ userId: account.id, username }) + account.token = await this.auth.generateAccessToken(account) account.createdAt = Date.now() const newUser = new User(account) @@ -150,7 +150,7 @@ class UserController { if (user.update(account)) { if (shouldUpdateToken) { - user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username }) + user.token = await this.auth.generateAccessToken(user) Logger.info(`[UserController] User ${user.username} was generated a new api token`) } await Database.updateUser(user) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 75e5a5f11..7d26b6bfc 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -31,52 +31,11 @@ class BookFinder { return book } - stripSubtitle(title) { - if (title.includes(':')) { - return title.split(':')[0].trim() - } else if (title.includes(' - ')) { - return title.split(' - ')[0].trim() - } - return title - } - - replaceAccentedChars(str) { - try { - return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "") - } catch (error) { - Logger.error('[BookFinder] str normalize error', error) - return str - } - } - - cleanTitleForCompares(title) { - if (!title) return '' - // Remove subtitle if there (i.e. "Cool Book: Coolest Ever" becomes "Cool Book") - let stripped = this.stripSubtitle(title) - - // Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game") - let cleaned = stripped.replace(/ *\([^)]*\) */g, "") - - // Remove single quotes (i.e. "Ender's Game" becomes "Enders Game") - cleaned = cleaned.replace(/'/g, '') - return this.replaceAccentedChars(cleaned).toLowerCase() - } - - cleanAuthorForCompares(author) { - if (!author) return '' - let cleanAuthor = this.replaceAccentedChars(author).toLowerCase() - // separate initials - cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2') - // remove middle initials - cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '') - return cleanAuthor - } - filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) { - var searchTitle = this.cleanTitleForCompares(title) - var searchAuthor = this.cleanAuthorForCompares(author) + var searchTitle = cleanTitleForCompares(title) + var searchAuthor = cleanAuthorForCompares(author) return books.map(b => { - b.cleanedTitle = this.cleanTitleForCompares(b.title) + b.cleanedTitle = cleanTitleForCompares(b.title) b.titleDistance = levenshteinDistance(b.cleanedTitle, title) // Total length of search (title or both title & author) @@ -87,7 +46,7 @@ class BookFinder { b.authorDistance = author.length } else { b.totalPossibleDistance += b.author.length - b.cleanedAuthor = this.cleanAuthorForCompares(b.author) + b.cleanedAuthor = cleanAuthorForCompares(b.author) var cleanedAuthorDistance = levenshteinDistance(b.cleanedAuthor, searchAuthor) var authorDistance = levenshteinDistance(b.author || '', author) @@ -190,20 +149,17 @@ class BookFinder { static TitleCandidates = class { - constructor(bookFinder, cleanAuthor) { - this.bookFinder = bookFinder + constructor(cleanAuthor) { this.candidates = new Set() this.cleanAuthor = cleanAuthor this.priorities = {} this.positions = {} + this.currentPosition = 0 } - add(title, position = 0) { + add(title) { // if title contains the author, remove it - if (this.cleanAuthor) { - const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g") - title = this.bookFinder.cleanAuthorForCompares(title).replace(authorRe, '').trim() - } + title = this.#removeAuthorFromTitle(title) const titleTransformers = [ [/([,:;_]| by ).*/g, ''], // Remove subtitle @@ -215,11 +171,11 @@ class BookFinder { ] // Main variant - const cleanTitle = this.bookFinder.cleanTitleForCompares(title).trim() + const cleanTitle = cleanTitleForCompares(title).trim() if (!cleanTitle) return this.candidates.add(cleanTitle) this.priorities[cleanTitle] = 0 - this.positions[cleanTitle] = position + this.positions[cleanTitle] = this.currentPosition let candidate = cleanTitle @@ -230,10 +186,11 @@ class BookFinder { if (candidate) { this.candidates.add(candidate) this.priorities[candidate] = 0 - this.positions[candidate] = position + this.positions[candidate] = this.currentPosition } this.priorities[cleanTitle] = 1 } + this.currentPosition++ } get size() { @@ -243,23 +200,16 @@ class BookFinder { getCandidates() { var candidates = [...this.candidates] candidates.sort((a, b) => { - // Candidates that include the author are likely low quality - const includesAuthorDiff = !b.includes(this.cleanAuthor) - !a.includes(this.cleanAuthor) - if (includesAuthorDiff) return includesAuthorDiff // Candidates that include only digits are also likely low quality const onlyDigits = /^\d+$/ - const includesOnlyDigitsDiff = !onlyDigits.test(b) - !onlyDigits.test(a) + const includesOnlyDigitsDiff = onlyDigits.test(a) - onlyDigits.test(b) if (includesOnlyDigitsDiff) return includesOnlyDigitsDiff // transformed candidates receive higher priority const priorityDiff = this.priorities[a] - this.priorities[b] if (priorityDiff) return priorityDiff // if same priorirty, prefer candidates that are closer to the beginning (e.g. titles before subtitles) const positionDiff = this.positions[a] - this.positions[b] - if (positionDiff) return positionDiff - // Start with longer candidaets, as they are likely more specific - const lengthDiff = b.length - a.length - if (lengthDiff) return lengthDiff - return b.localeCompare(a) + return positionDiff // candidates with same priority always have different positions }) Logger.debug(`[${this.constructor.name}] Found ${candidates.length} fuzzy title candidates`) Logger.debug(candidates) @@ -269,21 +219,32 @@ class BookFinder { delete(title) { return this.candidates.delete(title) } + + #removeAuthorFromTitle(title) { + if (!this.cleanAuthor) return title + const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g") + const authorCleanedTitle = cleanAuthorForCompares(title) + const authorCleanedTitleWithoutAuthor = authorCleanedTitle.replace(authorRe, '') + if (authorCleanedTitleWithoutAuthor !== authorCleanedTitle) { + return authorCleanedTitleWithoutAuthor.trim() + } + return title + } } static AuthorCandidates = class { - constructor(bookFinder, cleanAuthor) { - this.bookFinder = bookFinder + constructor(cleanAuthor, audnexus) { + this.audnexus = audnexus this.candidates = new Set() this.cleanAuthor = cleanAuthor if (cleanAuthor) this.candidates.add(cleanAuthor) } validateAuthor(name, region = '', maxLevenshtein = 2) { - return this.bookFinder.audnexus.authorASINsRequest(name, region).then((asins) => { + return this.audnexus.authorASINsRequest(name, region).then((asins) => { for (const [i, asin] of asins.entries()) { if (i > 10) break - let cleanName = this.bookFinder.cleanAuthorForCompares(asin.name) + let cleanName = cleanAuthorForCompares(asin.name) if (!cleanName) continue if (cleanName.includes(name)) return name if (name.includes(cleanName)) return cleanName @@ -294,7 +255,7 @@ class BookFinder { } add(author) { - const cleanAuthor = this.bookFinder.cleanAuthorForCompares(author).trim() + const cleanAuthor = cleanAuthorForCompares(author).trim() if (!cleanAuthor) return this.candidates.add(cleanAuthor) } @@ -362,10 +323,10 @@ class BookFinder { title = title.trim().toLowerCase() author = author?.trim().toLowerCase() || '' - const cleanAuthor = this.cleanAuthorForCompares(author) + const cleanAuthor = cleanAuthorForCompares(author) // Now run up to maxFuzzySearches fuzzy searches - let authorCandidates = new BookFinder.AuthorCandidates(this, cleanAuthor) + let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus) // Remove underscores and parentheses with their contents, and replace with a separator const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, " - ") @@ -375,9 +336,9 @@ class BookFinder { authorCandidates.add(titlePart) authorCandidates = await authorCandidates.getCandidates() for (const authorCandidate of authorCandidates) { - let titleCandidates = new BookFinder.TitleCandidates(this, authorCandidate) - for (const [position, titlePart] of titleParts.entries()) - titleCandidates.add(titlePart, position) + let titleCandidates = new BookFinder.TitleCandidates(authorCandidate) + for (const titlePart of titleParts) + titleCandidates.add(titlePart) titleCandidates = titleCandidates.getCandidates() for (const titleCandidate of titleCandidates) { if (titleCandidate == title && authorCandidate == author) continue // We already tried this @@ -457,3 +418,52 @@ class BookFinder { } } module.exports = new BookFinder() + +function stripSubtitle(title) { + if (title.includes(':')) { + return title.split(':')[0].trim() + } else if (title.includes(' - ')) { + return title.split(' - ')[0].trim() + } + return title +} + +function replaceAccentedChars(str) { + try { + return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "") + } catch (error) { + Logger.error('[BookFinder] str normalize error', error) + return str + } +} + +function cleanTitleForCompares(title) { + if (!title) return '' + title = stripRedundantSpaces(title) + + // Remove subtitle if there (i.e. "Cool Book: Coolest Ever" becomes "Cool Book") + let stripped = stripSubtitle(title) + + // Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game") + let cleaned = stripped.replace(/ *\([^)]*\) */g, "") + + // Remove single quotes (i.e. "Ender's Game" becomes "Enders Game") + cleaned = cleaned.replace(/'/g, '') + return replaceAccentedChars(cleaned).toLowerCase() +} + +function cleanAuthorForCompares(author) { + if (!author) return '' + author = stripRedundantSpaces(author) + + let cleanAuthor = replaceAccentedChars(author).toLowerCase() + // separate initials + cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2') + // remove middle initials + cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '') + return cleanAuthor +} + +function stripRedundantSpaces(str) { + return str.replace(/\s+/g, ' ').trim() +} diff --git a/server/libs/passportLocal/LICENSE b/server/libs/passportLocal/LICENSE new file mode 100644 index 000000000..d8ebfcf1c --- /dev/null +++ b/server/libs/passportLocal/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2011-2014 Jared Hanson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/server/libs/passportLocal/index.js b/server/libs/passportLocal/index.js new file mode 100644 index 000000000..365d4f65e --- /dev/null +++ b/server/libs/passportLocal/index.js @@ -0,0 +1,20 @@ +// +// modified for audiobookshelf +// Source: https://github.com/jaredhanson/passport-local +// + +/** + * Module dependencies. + */ +var Strategy = require('./strategy'); + + +/** + * Expose `Strategy` directly from package. + */ +exports = module.exports = Strategy; + +/** + * Export constructors. + */ +exports.Strategy = Strategy; diff --git a/server/libs/passportLocal/strategy.js b/server/libs/passportLocal/strategy.js new file mode 100644 index 000000000..671102044 --- /dev/null +++ b/server/libs/passportLocal/strategy.js @@ -0,0 +1,119 @@ +/** + * Module dependencies. + */ +const passport = require('passport-strategy') +const util = require('util') + + +function lookup(obj, field) { + if (!obj) { return null; } + var chain = field.split(']').join('').split('['); + for (var i = 0, len = chain.length; i < len; i++) { + var prop = obj[chain[i]]; + if (typeof (prop) === 'undefined') { return null; } + if (typeof (prop) !== 'object') { return prop; } + obj = prop; + } + return null; +} + +/** + * `Strategy` constructor. + * + * The local authentication strategy authenticates requests based on the + * credentials submitted through an HTML-based login form. + * + * Applications must supply a `verify` callback which accepts `username` and + * `password` credentials, and then calls the `done` callback supplying a + * `user`, which should be set to `false` if the credentials are not valid. + * If an exception occured, `err` should be set. + * + * Optionally, `options` can be used to change the fields in which the + * credentials are found. + * + * Options: + * - `usernameField` field name where the username is found, defaults to _username_ + * - `passwordField` field name where the password is found, defaults to _password_ + * - `passReqToCallback` when `true`, `req` is the first argument to the verify callback (default: `false`) + * + * Examples: + * + * passport.use(new LocalStrategy( + * function(username, password, done) { + * User.findOne({ username: username, password: password }, function (err, user) { + * done(err, user); + * }); + * } + * )); + * + * @param {Object} options + * @param {Function} verify + * @api public + */ +function Strategy(options, verify) { + if (typeof options == 'function') { + verify = options; + options = {}; + } + if (!verify) { throw new TypeError('LocalStrategy requires a verify callback'); } + + this._usernameField = options.usernameField || 'username'; + this._passwordField = options.passwordField || 'password'; + + passport.Strategy.call(this); + this.name = 'local'; + this._verify = verify; + this._passReqToCallback = options.passReqToCallback; +} + +/** + * Inherit from `passport.Strategy`. + */ +util.inherits(Strategy, passport.Strategy); + +/** + * Authenticate request based on the contents of a form submission. + * + * @param {Object} req + * @api protected + */ +Strategy.prototype.authenticate = function (req, options) { + options = options || {}; + var username = lookup(req.body, this._usernameField) + if (username === null) { + lookup(req.query, this._usernameField); + } + + var password = lookup(req.body, this._passwordField) + if (password === null) { + password = lookup(req.query, this._passwordField); + } + + if (username === null || password === null) { + return this.fail({ message: options.badRequestMessage || 'Missing credentials' }, 400); + } + + var self = this; + + function verified(err, user, info) { + if (err) { return self.error(err); } + if (!user) { return self.fail(info); } + self.success(user, info); + } + + try { + if (self._passReqToCallback) { + this._verify(req, username, password, verified); + } else { + this._verify(username, password, verified); + } + } catch (ex) { + return self.error(ex); + } +}; + + +/** + * Expose `Strategy`. + */ +module.exports = Strategy; diff --git a/server/models/User.js b/server/models/User.js index bf22a3a58..4c348f42f 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -1,7 +1,9 @@ const uuidv4 = require("uuid").v4 -const { DataTypes, Model, Op } = require('sequelize') +const sequelize = require('sequelize') const Logger = require('../Logger') const oldUser = require('../objects/user/User') +const SocketAuthority = require('../SocketAuthority') +const { DataTypes, Model } = sequelize class User extends Model { constructor(values, options) { @@ -46,6 +48,12 @@ class User extends Model { return users.map(u => this.getOldUser(u)) } + /** + * Get old user model from new + * + * @param {Object} userExpanded + * @returns {oldUser} + */ static getOldUser(userExpanded) { const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress()) @@ -72,15 +80,27 @@ class User extends Model { createdAt: userExpanded.createdAt.valueOf(), permissions, librariesAccessible, - itemTagsSelected + itemTagsSelected, + authOpenIDSub: userExpanded.extraData?.authOpenIDSub || null }) } + /** + * + * @param {oldUser} oldUser + * @returns {Promise<User>} + */ static createFromOld(oldUser) { const user = this.getFromOld(oldUser) return this.create(user) } + /** + * Update User from old user model + * + * @param {oldUser} oldUser + * @returns {Promise<boolean>} + */ static updateFromOld(oldUser) { const user = this.getFromOld(oldUser) return this.update(user, { @@ -93,7 +113,21 @@ class User extends Model { }) } + /** + * Get new User model from old + * + * @param {oldUser} oldUser + * @returns {Object} + */ static getFromOld(oldUser) { + const extraData = { + seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], + oldUserId: oldUser.oldUserId + } + if (oldUser.authOpenIDSub) { + extraData.authOpenIDSub = oldUser.authOpenIDSub + } + return { id: oldUser.id, username: oldUser.username, @@ -103,10 +137,7 @@ class User extends Model { token: oldUser.token || null, isActive: !!oldUser.isActive, lastSeen: oldUser.lastSeen || null, - extraData: { - seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], - oldUserId: oldUser.oldUserId - }, + extraData, createdAt: oldUser.createdAt || Date.now(), permissions: { ...oldUser.permissions, @@ -130,12 +161,12 @@ class User extends Model { * @param {string} username * @param {string} pash * @param {Auth} auth - * @returns {oldUser} + * @returns {Promise<oldUser>} */ static async createRootUser(username, pash, auth) { const userId = uuidv4() - const token = await auth.generateAccessToken({ userId, username }) + const token = await auth.generateAccessToken({ id: userId, username }) const newRoot = new oldUser({ id: userId, @@ -150,6 +181,38 @@ class User extends Model { return newRoot } + /** + * Create user from openid userinfo + * @param {Object} userinfo + * @param {Auth} auth + * @returns {Promise<oldUser>} + */ + static async createUserFromOpenIdUserInfo(userinfo, auth) { + const userId = uuidv4() + // TODO: Ensure username is unique? + const username = userinfo.preferred_username || userinfo.name || userinfo.sub + const email = (userinfo.email && userinfo.email_verified) ? userinfo.email : null + + const token = await auth.generateAccessToken({ id: userId, username }) + + const newUser = new oldUser({ + id: userId, + type: 'user', + username, + email, + pash: null, + token, + isActive: true, + authOpenIDSub: userinfo.sub, + createdAt: Date.now() + }) + if (await this.createFromOld(newUser)) { + SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser()) + return newUser + } + return null + } + /** * Get a user by id or by the old database id * @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id @@ -160,13 +223,13 @@ class User extends Model { if (!userId) return null const user = await this.findOne({ where: { - [Op.or]: [ + [sequelize.Op.or]: [ { id: userId }, { extraData: { - [Op.substring]: userId + [sequelize.Op.substring]: userId } } ] @@ -187,7 +250,26 @@ class User extends Model { const user = await this.findOne({ where: { username: { - [Op.like]: username + [sequelize.Op.like]: username + } + }, + include: this.sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + + /** + * Get user by email case insensitive + * @param {string} username + * @returns {Promise<oldUser|null>} returns null if not found + */ + static async getUserByEmail(email) { + if (!email) return null + const user = await this.findOne({ + where: { + email: { + [sequelize.Op.like]: email } }, include: this.sequelize.models.mediaProgress @@ -210,6 +292,21 @@ class User extends Model { return this.getOldUser(user) } + /** + * Get user by openid sub + * @param {string} sub + * @returns {Promise<oldUser|null>} returns null if not found + */ + static async getUserByOpenIDSub(sub) { + if (!sub) return null + const user = await this.findOne({ + where: sequelize.where(sequelize.literal(`extraData->>"authOpenIDSub"`), sub), + include: this.sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + /** * Get array of user id and username * @returns {object[]} { id, username } diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index f31aaf6be..bf3db5577 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -54,6 +54,24 @@ class ServerSettings { this.version = packageJson.version this.buildNumber = packageJson.buildNumber + // Auth settings + // Active auth methodes + this.authActiveAuthMethods = ['local'] + + // openid settings + this.authOpenIDIssuerURL = null + this.authOpenIDAuthorizationURL = null + this.authOpenIDTokenURL = null + this.authOpenIDUserInfoURL = null + this.authOpenIDJwksURL = null + this.authOpenIDLogoutURL = null + this.authOpenIDClientID = null + this.authOpenIDClientSecret = null + this.authOpenIDButtonText = 'Login with OpenId' + this.authOpenIDAutoLaunch = false + this.authOpenIDAutoRegister = false + this.authOpenIDMatchExistingBy = null + if (settings) { this.construct(settings) } @@ -94,6 +112,36 @@ class ServerSettings { this.version = settings.version || null this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 + this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] + + this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null + this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || null + this.authOpenIDTokenURL = settings.authOpenIDTokenURL || null + this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || null + this.authOpenIDJwksURL = settings.authOpenIDJwksURL || null + this.authOpenIDLogoutURL = settings.authOpenIDLogoutURL || null + this.authOpenIDClientID = settings.authOpenIDClientID || null + this.authOpenIDClientSecret = settings.authOpenIDClientSecret || null + this.authOpenIDButtonText = settings.authOpenIDButtonText || 'Login with OpenId' + this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch + this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister + this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null + + if (!Array.isArray(this.authActiveAuthMethods)) { + this.authActiveAuthMethods = ['local'] + } + + // remove uninitialized methods + // OpenID + if (this.authActiveAuthMethods.includes('openid') && !this.isOpenIDAuthSettingsValid) { + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('openid', 0), 1) + } + + // fallback to local + if (!Array.isArray(this.authActiveAuthMethods) || this.authActiveAuthMethods.length == 0) { + this.authActiveAuthMethods = ['local'] + } + // Migrations if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was renamed to storeCoverWithItem in 2.0.0 this.storeCoverWithItem = !!settings.storeCoverWithBook @@ -150,23 +198,96 @@ class ServerSettings { language: this.language, logLevel: this.logLevel, version: this.version, - buildNumber: this.buildNumber + buildNumber: this.buildNumber, + authActiveAuthMethods: this.authActiveAuthMethods, + authOpenIDIssuerURL: this.authOpenIDIssuerURL, + authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, + authOpenIDTokenURL: this.authOpenIDTokenURL, + authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, + authOpenIDJwksURL: this.authOpenIDJwksURL, + authOpenIDLogoutURL: this.authOpenIDLogoutURL, + authOpenIDClientID: this.authOpenIDClientID, // Do not return to client + authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client + authOpenIDButtonText: this.authOpenIDButtonText, + authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, + authOpenIDAutoRegister: this.authOpenIDAutoRegister, + authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy } } toJSONForBrowser() { const json = this.toJSON() delete json.tokenSecret + delete json.authOpenIDClientID + delete json.authOpenIDClientSecret return json } + get supportedAuthMethods() { + return ['local', 'openid'] + } + + /** + * Auth settings required for openid to be valid + */ + get isOpenIDAuthSettingsValid() { + return this.authOpenIDIssuerURL && + this.authOpenIDAuthorizationURL && + this.authOpenIDTokenURL && + this.authOpenIDUserInfoURL && + this.authOpenIDJwksURL && + this.authOpenIDClientID && + this.authOpenIDClientSecret + } + + get authenticationSettings() { + return { + authActiveAuthMethods: this.authActiveAuthMethods, + authOpenIDIssuerURL: this.authOpenIDIssuerURL, + authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, + authOpenIDTokenURL: this.authOpenIDTokenURL, + authOpenIDUserInfoURL: this.authOpenIDUserInfoURL, + authOpenIDJwksURL: this.authOpenIDJwksURL, + authOpenIDLogoutURL: this.authOpenIDLogoutURL, + authOpenIDClientID: this.authOpenIDClientID, // Do not return to client + authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client + authOpenIDButtonText: this.authOpenIDButtonText, + authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, + authOpenIDAutoRegister: this.authOpenIDAutoRegister, + authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy + } + } + + get authFormData() { + const clientFormData = {} + if (this.authActiveAuthMethods.includes('openid')) { + clientFormData.authOpenIDButtonText = this.authOpenIDButtonText + clientFormData.authOpenIDAutoLaunch = this.authOpenIDAutoLaunch + } + return clientFormData + } + + /** + * Update server settings + * + * @param {Object} payload + * @returns {boolean} true if updates were made + */ update(payload) { - var hasUpdates = false + let hasUpdates = false for (const key in payload) { - if (key === 'sortingPrefixes' && payload[key] && payload[key].length) { - var prefixesCleaned = payload[key].filter(prefix => !!prefix).map(prefix => prefix.toLowerCase()) - if (prefixesCleaned.join(',') !== this[key].join(',')) { - this[key] = [...prefixesCleaned] + if (key === 'sortingPrefixes') { + // Sorting prefixes are updated with the /api/sorting-prefixes endpoint + continue + } else if (key === 'authActiveAuthMethods') { + if (!payload[key]?.length) { + Logger.error(`[ServerSettings] Invalid authActiveAuthMethods`, payload[key]) + continue + } + this.authActiveAuthMethods.sort() + payload[key].sort() + if (payload[key].join() !== this.authActiveAuthMethods.join()) { + this.authActiveAuthMethods = payload[key] hasUpdates = true } } else if (this[key] !== payload[key]) { diff --git a/server/objects/user/User.js b/server/objects/user/User.js index 5192752a3..b503872d6 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -24,6 +24,8 @@ class User { this.librariesAccessible = [] // Library IDs (Empty if ALL libraries) this.itemTagsSelected = [] // Empty if ALL item tags accessible + this.authOpenIDSub = null + if (user) { this.construct(user) } @@ -66,7 +68,7 @@ class User { getDefaultUserPermissions() { return { download: true, - update: true, + update: this.type === 'root' || this.type === 'admin', delete: this.type === 'root', upload: this.type === 'root' || this.type === 'admin', accessAllLibraries: true, @@ -93,7 +95,8 @@ class User { createdAt: this.createdAt, permissions: this.permissions, librariesAccessible: [...this.librariesAccessible], - itemTagsSelected: [...this.itemTagsSelected] + itemTagsSelected: [...this.itemTagsSelected], + authOpenIDSub: this.authOpenIDSub } } @@ -186,6 +189,8 @@ class User { this.librariesAccessible = [...(user.librariesAccessible || [])] this.itemTagsSelected = [...(user.itemTagsSelected || [])] + + this.authOpenIDSub = user.authOpenIDSub || null } update(payload) { diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index e499b2cf3..b18882951 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -36,6 +36,7 @@ const { measureMiddleware } = require('../utils/timing') class ApiRouter { constructor(Server) { + /** @type {import('../Auth')} */ this.auth = Server.auth this.playbackSessionManager = Server.playbackSessionManager this.abMergeManager = Server.abMergeManager @@ -312,6 +313,8 @@ class ApiRouter { this.router.post('/genres/rename', MiscController.renameGenre.bind(this)) this.router.delete('/genres/:genre', MiscController.deleteGenre.bind(this)) this.router.post('/validate-cron', MiscController.validateCronExpression.bind(this)) + this.router.get('/auth-settings', MiscController.getAuthSettings.bind(this)) + this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this)) this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this)) } diff --git a/test/server/finders/BookFinder.test.js b/test/server/finders/BookFinder.test.js new file mode 100644 index 000000000..2728f174a --- /dev/null +++ b/test/server/finders/BookFinder.test.js @@ -0,0 +1,344 @@ +const sinon = require('sinon') +const chai = require('chai') +const expect = chai.expect +const bookFinder = require('../../../server/finders/BookFinder') +const { LogLevel } = require('../../../server/utils/constants') +const Logger = require('../../../server/Logger') +Logger.setLogLevel(LogLevel.INFO) + +describe('TitleCandidates', () => { + describe('cleanAuthor non-empty', () => { + let titleCandidates + const cleanAuthor = 'leo tolstoy' + + beforeEach(() => { + titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) + }) + + describe('no adds', () => { + it('returns no candidates', () => { + expect(titleCandidates.getCandidates()).to.deep.equal([]) + }) + }) + + describe('single add', () => { + [ + ['adds candidate', 'anna karenina', ['anna karenina']], + ['adds lowercased candidate', 'ANNA KARENINA', ['anna karenina']], + ['adds candidate, removing redundant spaces', 'anna karenina', ['anna karenina']], + ['adds candidate, removing author', `anna karenina by ${cleanAuthor}`, ['anna karenina']], + ['does not add empty candidate after removing author', cleanAuthor, []], + ['adds candidate, removing subtitle', 'anna karenina: subtitle', ['anna karenina']], + ['adds candidate + variant, removing "by ..."', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], + ['adds candidate + variant, removing bitrate', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], + ['adds candidate + variant, removing edition 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], + ['adds candidate + variant, removing edition 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], + ['adds candidate + variant, removing fie type', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']], + ['adds candidate + variant, removing "a novel"', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']], + ['adds candidate + variant, removing preceding/trailing numbers', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']], + ['does not add empty candidate', '', []], + ['does not add spaces-only candidate', ' ', []], + ['does not add empty variant', '1984', ['1984']], + ].forEach(([name, title, expected]) => it(name, () => { + titleCandidates.add(title) + expect(titleCandidates.getCandidates()).to.deep.equal(expected) + })) + }) + + describe('multiple adds', () => { + [ + ['demotes digits-only candidates', ['01', 'anna karenina'], ['anna karenina', '01']], + ['promotes transformed variants', ['title1 1', 'title2 1'], ['title1', 'title2', 'title1 1', 'title2 1']], + ['orders by position', ['title2', 'title1'], ['title2', 'title1']], + ['dedupes candidates', ['title1', 'title1'], ['title1']], + ].forEach(([name, titles, expected]) => it(name, () => { + for (const title of titles) titleCandidates.add(title) + expect(titleCandidates.getCandidates()).to.deep.equal(expected) + })) + }) + }) + + describe('cleanAuthor empty', () => { + let titleCandidates + let cleanAuthor = '' + + beforeEach(() => { + titleCandidates = new bookFinder.constructor.TitleCandidates(cleanAuthor) + }) + + describe('single add', () => { + [ + ['adds a candidate', 'leo tolstoy', ['leo tolstoy']], + ].forEach(([name, title, expected]) => it(name, () => { + titleCandidates.add(title) + expect(titleCandidates.getCandidates()).to.deep.equal(expected) + })) + }) + }) +}) + +describe('AuthorCandidates', () => { + let authorCandidates + const audnexus = { + authorASINsRequest: sinon.stub().resolves([ + { name: 'Leo Tolstoy' }, + { name: 'Nikolai Gogol' }, + { name: 'J. K. Rowling' }, + ]), + } + + describe('cleanAuthor is null', () => { + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(null, audnexus) + }) + + describe('no adds', () => { + [ + ['returns empty author candidate', []], + ].forEach(([name, expected]) => it(name, async () => { + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('single add', () => { + [ + ['adds recognized candidate', 'nikolai gogol', ['nikolai gogol']], + ['does not add unrecognized candidate', 'fyodor dostoevsky', []], + ['adds recognized author if candidate is a superstring', 'dr. nikolai gogol', ['nikolai gogol']], + ['adds candidate if it is a substring of recognized author', 'gogol', ['gogol']], + ['adds recognized author if edit distance from candidate is small', 'nicolai gogol', ['nikolai gogol']], + ['does not add candidate if edit distance from any recognized author is large', 'nikolai google', []], + ['adds normalized recognized candidate (contains redundant spaces)', 'nikolai gogol', ['nikolai gogol']], + ['adds normalized recognized candidate (normalized initials)', 'j.k. rowling', ['j. k. rowling']], + ].forEach(([name, author, expected]) => it(name, async () => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('multi add', () => { + [ + ['adds recognized author candidates', ['nikolai gogol', 'leo tolstoy'], ['nikolai gogol', 'leo tolstoy']], + ['dedupes author candidates', ['nikolai gogol', 'nikolai gogol'], ['nikolai gogol']], + ].forEach(([name, authors, expected]) => it(name, async () => { + for (const author of authors) authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }) + + describe('cleanAuthor is a recognized author', () => { + const cleanAuthor = 'leo tolstoy' + + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + }) + + describe('no adds', () => { + [ + ['adds cleanAuthor as candidate', [cleanAuthor]], + ].forEach(([name, expected]) => it(name, async () => { + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('single add', () => { + [ + ['adds recognized candidate', 'nikolai gogol', [cleanAuthor, 'nikolai gogol']], + ['does not add candidate if it is a dupe of cleanAuthor', cleanAuthor, [cleanAuthor]], + ].forEach(([name, author, expected]) => it(name, async () => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }) + + describe('cleanAuthor is an unrecognized author', () => { + const cleanAuthor = 'Fyodor Dostoevsky' + + beforeEach(() => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + }) + + describe('no adds', () => { + [ + ['adds cleanAuthor as candidate', [cleanAuthor]], + ].forEach(([name, expected]) => it(name, async () => { + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('single add', () => { + [ + ['adds recognized candidate and removes cleanAuthor', 'nikolai gogol', ['nikolai gogol']], + ['does not add unrecognized candidate', 'jackie chan', [cleanAuthor]], + ].forEach(([name, author, expected]) => it(name, async () => { + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }) + + describe('cleanAuthor is unrecognized and dirty', () => { + describe('no adds', () => { + [ + ['adds aggressively cleaned cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', ['fyodor dostoevsky']], + ['adds cleanAuthor if aggresively cleaned cleanAuthor is empty', ', jackie chan', [', jackie chan']], + ].forEach(([name, cleanAuthor, expected]) => it(name, async () => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + + describe('single add', () => { + [ + ['adds recognized candidate and removes cleanAuthor', 'fyodor dostoevsky, translated by jackie chan', 'nikolai gogol', ['nikolai gogol']], + ].forEach(([name, cleanAuthor, author, expected]) => it(name, async () => { + authorCandidates = new bookFinder.constructor.AuthorCandidates(cleanAuthor, audnexus) + authorCandidates.add(author) + expect(await authorCandidates.getCandidates()).to.deep.equal([...expected, '']) + })) + }) + }) +}) + +describe('search', () => { + const t = 'title' + const a = 'author' + const u = 'unrecognized' + const r = ['book'] + + const runSearchStub = sinon.stub(bookFinder, 'runSearch') + runSearchStub.resolves([]) + runSearchStub.withArgs(t, a).resolves(r) + runSearchStub.withArgs(t, u).resolves(r) + + const audnexusStub = sinon.stub(bookFinder.audnexus, 'authorASINsRequest') + audnexusStub.resolves([{ name: a }]) + + beforeEach(() => { + bookFinder.runSearch.resetHistory() + }) + + describe('search title is empty', () => { + it('returns empty result', async () => { + expect(await bookFinder.search('', '', a)).to.deep.equal([]) + sinon.assert.callCount(bookFinder.runSearch, 0) + }) + }) + + describe('search title is a recognized title and search author is a recognized author', () => { + it('returns non-empty result (no fuzzy searches)', async () => { + expect(await bookFinder.search('', t, a)).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 1) + }) + }) + + describe('search title contains recognized title and search author is a recognized author', () => { + [ + [`${t} -`], + [`${t} - ${a}`], + [`${a} - ${t}`], + [`${t}- ${a}`], + [`${t} -${a}`], + [`${t} ${a}`], + [`${a} - ${t} (unabridged)`], + [`${a} - ${t} (subtitle) - mp3`], + [`${t} {narrator} - series-01 64kbps 10:00:00`], + [`${a} - ${t} (2006) narrated by narrator [unabridged]`], + [`${t} - ${a} 2022 mp3`], + [`01 ${t}`], + [`2022_${t}_HQ`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns non-empty result (with 1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 2) + }) + }); + + [ + [`s-01 - ${t} (narrator) 64kbps 10:00:00`], + [`${a} - series 01 - ${t}`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns non-empty result (with 2 fuzzy searches)`, async () => { + expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 3) + }) + }); + + [ + [`${t}-${a}`], + [`${t} junk`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns an empty result`, async () => { + expect(await bookFinder.search('', searchTitle, a)).to.deep.equal([]) + }) + }) + + describe('maxFuzzySearches = 0', () => { + [ + [`${t} - ${a}`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns an empty result (with no fuzzy searches)`, async () => { + expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 0 })).to.deep.equal([]) + sinon.assert.callCount(bookFinder.runSearch, 1) + }) + }) + }) + + describe('maxFuzzySearches = 1', () => { + [ + [`s-01 - ${t} (narrator) 64kbps 10:00:00`], + [`${a} - series 01 - ${t}`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${a}') returns an empty result (1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 1 })).to.deep.equal([]) + sinon.assert.callCount(bookFinder.runSearch, 2) + }) + }) + }) + }) + + describe('search title contains recognized title and search author is empty', () => { + [ + [`${t} - ${a}`], + [`${a} - ${t}`], + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '') returns a non-empty result (1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, '')).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 2) + }) + }); + + [ + [`${t}`], + [`${t} - ${u}`], + [`${u} - ${t}`] + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '') returns an empty result`, async () => { + expect(await bookFinder.search('', searchTitle, '')).to.deep.equal([]) + }) + }) + }) + + describe('search title contains recognized title and search author is an unrecognized author', () => { + [ + [`${t} - ${u}`], + [`${u} - ${t}`] + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${u}') returns a non-empty result (1 fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 2) + }) + }); + + [ + [`${t}`] + ].forEach(([searchTitle]) => { + it(`search('${searchTitle}', '${u}') returns a non-empty result (no fuzzy search)`, async () => { + expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r) + sinon.assert.callCount(bookFinder.runSearch, 1) + }) + }) + }) +}) From 32ce771911bf4356625cd853b2d69566aacd9601 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 22 Nov 2023 12:37:18 -0600 Subject: [PATCH 170/208] Allow cors while in development --- server/Server.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/Server.js b/server/Server.js index 1397bbd19..e7be5492e 100644 --- a/server/Server.js +++ b/server/Server.js @@ -138,11 +138,13 @@ class Server { * The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests * so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint * @see https://ionicframework.com/docs/troubleshooting/cors + * + * Running in development allows cors to allow testing the mobile apps in the browser */ app.use((req, res, next) => { - if (req.path.match(/\/api\/items\/([a-z0-9-]{36})\/ebook(\/[0-9]+)?/)) { + if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/ebook(\/[0-9]+)?/)) { const allowedOrigins = ['capacitor://localhost', 'http://localhost'] - if (allowedOrigins.some(o => o === req.get('origin'))) { + if (Logger.isDev || allowedOrigins.some(o => o === req.get('origin'))) { res.header('Access-Control-Allow-Origin', req.get('origin')) res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS') res.header('Access-Control-Allow-Headers', '*') From 288beae8740343268e83530999857e00187ff5c6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 22 Nov 2023 12:38:11 -0600 Subject: [PATCH 171/208] Update:OIDC auth auto launch setting description to include manual override path --- client/pages/config/authentication.vue | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 9ea8172ab..a71f62e8b 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -1,5 +1,5 @@ <template> - <div> + <div id="authentication-settings"> <app-settings-content :header-text="$strings.HeaderAuthentication"> <div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25"> <div class="flex items-center"> @@ -52,13 +52,13 @@ <div class="flex items-center py-4 px-1"> <ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" /> - <p id="auto-redirect-toggle" class="pl-4">Auto Launch</p> - <p class="pl-4 text-sm text-gray-300">Redirect to the auth provider automatically when navigating to the login page</p> + <p id="auto-redirect-toggle" class="pl-4 whitespace-nowrap">Auto Launch</p> + <p class="pl-4 text-sm text-gray-300">Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)</p> </div> <div class="flex items-center py-4 px-1"> <ui-toggle-switch labeledBy="auto-register-toggle" v-model="newAuthSettings.authOpenIDAutoRegister" :disabled="savingSettings" /> - <p id="auto-register-toggle" class="pl-4">Auto Register</p> + <p id="auto-register-toggle" class="pl-4 whitespace-nowrap">Auto Register</p> <p class="pl-4 text-sm text-gray-300">Automatically create new users after logging in</p> </div> </div> @@ -227,3 +227,13 @@ export default { } </script> +<style> +#authentication-settings code { + font-size: 0.8rem; + border-radius: 6px; + background-color: rgb(82, 82, 82); + color: white; + padding: 2px 4px; + white-space: nowrap; +} +</style> \ No newline at end of file From 6651ad0d45bbcd43b38048d86f255095745052c7 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 22 Nov 2023 12:55:01 -0600 Subject: [PATCH 172/208] Update:Added translation strings for OIDC auth --- client/pages/config/authentication.vue | 18 +++++++++--------- client/strings/cs.json | 12 +++++++++++- client/strings/da.json | 10 ++++++++++ client/strings/de.json | 10 ++++++++++ client/strings/en-us.json | 9 +++++++++ client/strings/es.json | 10 ++++++++++ client/strings/fr.json | 10 ++++++++++ client/strings/gu.json | 10 ++++++++++ client/strings/hi.json | 10 ++++++++++ client/strings/hr.json | 10 ++++++++++ client/strings/it.json | 12 +++++++++++- client/strings/lt.json | 10 ++++++++++ client/strings/nl.json | 10 ++++++++++ client/strings/no.json | 10 ++++++++++ client/strings/pl.json | 10 ++++++++++ client/strings/ru.json | 10 ++++++++++ client/strings/sv.json | 10 ++++++++++ client/strings/zh-cn.json | 10 ++++++++++ 18 files changed, 180 insertions(+), 11 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index a71f62e8b..e2f6d6788 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -4,13 +4,13 @@ <div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25"> <div class="flex items-center"> <ui-checkbox v-model="enableLocalAuth" checkbox-bg="bg" /> - <p class="text-lg pl-4">Password Authentication</p> + <p class="text-lg pl-4">{{ $strings.HeaderPasswordAuthentication }}</p> </div> </div> <div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25"> <div class="flex items-center"> <ui-checkbox v-model="enableOpenIDAuth" checkbox-bg="bg" /> - <p class="text-lg pl-4">OpenID Connect Authentication</p> + <p class="text-lg pl-4">{{ $strings.HeaderOpenIDConnectAuthentication }}</p> </div> <transition name="slide"> @@ -41,25 +41,25 @@ <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" /> - <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="'Button Text'" class="mb-2" /> + <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" /> <div class="flex items-center pt-1 mb-2"> <div class="w-44"> - <ui-dropdown v-model="newAuthSettings.authOpenIDMatchExistingBy" small :items="matchingExistingOptions" label="Match existing users by" :disabled="savingSettings" /> + <ui-dropdown v-model="newAuthSettings.authOpenIDMatchExistingBy" small :items="matchingExistingOptions" :label="$strings.LabelMatchExistingUsersBy" :disabled="savingSettings" /> </div> - <p class="pl-4 text-sm text-gray-300 mt-5">Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider</p> + <p class="pl-4 text-sm text-gray-300 mt-5">{{ $strings.LabelMatchExistingUsersByDescription }}</p> </div> <div class="flex items-center py-4 px-1"> <ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" /> - <p id="auto-redirect-toggle" class="pl-4 whitespace-nowrap">Auto Launch</p> - <p class="pl-4 text-sm text-gray-300">Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)</p> + <p id="auto-redirect-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoLaunch }}</p> + <p class="pl-4 text-sm text-gray-300" v-html="$strings.LabelAutoLaunchDescription" /> </div> <div class="flex items-center py-4 px-1"> <ui-toggle-switch labeledBy="auto-register-toggle" v-model="newAuthSettings.authOpenIDAutoRegister" :disabled="savingSettings" /> - <p id="auto-register-toggle" class="pl-4 whitespace-nowrap">Auto Register</p> - <p class="pl-4 text-sm text-gray-300">Automatically create new users after logging in</p> + <p id="auto-register-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoRegister }}</p> + <p class="pl-4 text-sm text-gray-300">{{ $strings.LabelAutoRegisterDescription }}</p> </div> </div> </transition> diff --git a/client/strings/cs.json b/client/strings/cs.json index 07f3d4f7d..26e7bcc9d 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise", "HeaderAudiobookTools": "Nástroje pro správu souborů audioknih", "HeaderAudioTracks": "Zvukové stopy", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Zálohy", "HeaderChangePassword": "Změnit heslo", "HeaderChapters": "Kapitoly", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Nový účet", "HeaderNewLibrary": "Nová knihovna", "HeaderNotifications": "Oznámení", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Otevřít RSS kanál", "HeaderOtherFiles": "Ostatní soubory", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Oprávnění", "HeaderPlayerQueue": "Fronta přehrávače", "HeaderPlaylist": "Seznam skladeb", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Autor (příjmení a jméno)", "LabelAuthors": "Autoři", "LabelAutoDownloadEpisodes": "Automaticky stahovat epizody", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Zpět k uživateli", "LabelBackupLocation": "Umístění zálohy", "LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Najednou bude odstraněna pouze 1 záloha, takže pokud již máte více záloh, měli byste je odstranit ručně.", "LabelBitrate": "Datový tok", "LabelBooks": "Knihy", + "LabelButtonText": "Button Text", "LabelChangePassword": "Změnit heslo", "LabelChannels": "Kanály", "LabelChapters": "Kapitoly", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Informace", "LabelLogLevelWarn": "Varovat", "LabelLookForNewEpisodesAfterDate": "Hledat nové epizody po tomto datu", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Přehrávač médií", "LabelMediaType": "Typ média", "LabelMetadataOrderOfPrecedenceDescription": "1 je nejnižší priorita, 5 je nejvyšší priorita", @@ -726,4 +736,4 @@ "ToastSocketFailedToConnect": "Socket se nepodařilo připojit", "ToastUserDeleteFailed": "Nepodařilo se smazat uživatele", "ToastUserDeleteSuccess": "Uživatel smazán" -} +} \ No newline at end of file diff --git a/client/strings/da.json b/client/strings/da.json index 768bb724f..d950be165 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger", "HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer", "HeaderAudioTracks": "Lydspor", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Sikkerhedskopier", "HeaderChangePassword": "Skift Adgangskode", "HeaderChapters": "Kapitler", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Ny Konto", "HeaderNewLibrary": "Nyt Bibliotek", "HeaderNotifications": "Meddelelser", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Åbn RSS Feed", "HeaderOtherFiles": "Andre Filer", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Tilladelser", "HeaderPlayerQueue": "Afspilningskø", "HeaderPlaylist": "Afspilningsliste", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)", "LabelAuthors": "Forfattere", "LabelAutoDownloadEpisodes": "Auto Download Episoder", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Tilbage til Bruger", "LabelBackupLocation": "Backup Placering", "LabelBackupsEnableAutomaticBackups": "Aktivér automatisk sikkerhedskopiering", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Kun 1 sikkerhedskopi fjernes ad gangen, så hvis du allerede har flere sikkerhedskopier end dette, skal du fjerne dem manuelt.", "LabelBitrate": "Bitrate", "LabelBooks": "Bøger", + "LabelButtonText": "Button Text", "LabelChangePassword": "Ændre Adgangskode", "LabelChannels": "Kanaler", "LabelChapters": "Kapitler", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Information", "LabelLogLevelWarn": "Advarsel", "LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Medieafspiller", "LabelMediaType": "Medietype", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/de.json b/client/strings/de.json index f7cf8b68e..cb399ca42 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen", "HeaderAudiobookTools": "Hörbuch-Dateiverwaltungstools", "HeaderAudioTracks": "Audiodateien", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Sicherungen", "HeaderChangePassword": "Passwort ändern", "HeaderChapters": "Kapitel", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Neues Konto", "HeaderNewLibrary": "Neue Bibliothek", "HeaderNotifications": "Benachrichtigungen", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "RSS-Feed öffnen", "HeaderOtherFiles": "Sonstige Dateien", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Berechtigungen", "HeaderPlayerQueue": "Spieler Warteschlange", "HeaderPlaylist": "Wiedergabeliste", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Autor (Nachname, Vorname)", "LabelAuthors": "Autoren", "LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Zurück zum Benutzer", "LabelBackupLocation": "Backup-Ort", "LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn Sie bereits mehrere Sicherungen als die definierte max. Anzahl haben, sollten Sie diese manuell entfernen.", "LabelBitrate": "Bitrate", "LabelBooks": "Bücher", + "LabelButtonText": "Button Text", "LabelChangePassword": "Passwort ändern", "LabelChannels": "Kanäle", "LabelChapters": "Kapitel", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Informationen", "LabelLogLevelWarn": "Warnungen", "LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Mediaplayer", "LabelMediaType": "Medientyp", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 6f06ca77e..b78962816 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -132,8 +132,10 @@ "HeaderNewAccount": "New Account", "HeaderNewLibrary": "New Library", "HeaderNotifications": "Notifications", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Open RSS Feed", "HeaderOtherFiles": "Other Files", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Permissions", "HeaderPlayerQueue": "Player Queue", "HeaderPlaylist": "Playlist", @@ -194,6 +196,10 @@ "LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthors": "Authors", "LabelAutoDownloadEpisodes": "Auto Download Episodes", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Back to User", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups", @@ -204,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", "LabelBitrate": "Bitrate", "LabelBooks": "Books", + "LabelButtonText": "Button Text", "LabelChangePassword": "Change Password", "LabelChannels": "Channels", "LabelChapters": "Chapters", @@ -317,6 +324,8 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/es.json b/client/strings/es.json index 0ac0a9601..fde3782e6 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Ajustes de Notificaciones de Apprise", "HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro", "HeaderAudioTracks": "Pistas de Audio", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Respaldos", "HeaderChangePassword": "Cambiar Contraseña", "HeaderChapters": "Capítulos", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Nueva Cuenta", "HeaderNewLibrary": "Nueva Biblioteca", "HeaderNotifications": "Notificaciones", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Abrir fuente RSS", "HeaderOtherFiles": "Otros Archivos", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Permisos", "HeaderPlayerQueue": "Fila del Reproductor", "HeaderPlaylist": "Lista de Reproducción", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Autor (Apellido, Nombre)", "LabelAuthors": "Autores", "LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Regresar a Usuario", "LabelBackupLocation": "Ubicación del Respaldo", "LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.", "LabelBitrate": "Bitrate", "LabelBooks": "Libros", + "LabelButtonText": "Button Text", "LabelChangePassword": "Cambiar Contraseña", "LabelChannels": "Canales", "LabelChapters": "Capítulos", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Información", "LabelLogLevelWarn": "Advertencia", "LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Reproductor de Medios", "LabelMediaType": "Tipo de Multimedia", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/fr.json b/client/strings/fr.json index 5ad80723f..97d2766ec 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise", "HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook", "HeaderAudioTracks": "Pistes audio", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Sauvegardes", "HeaderChangePassword": "Modifier le mot de passe", "HeaderChapters": "Chapitres", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Nouveau compte", "HeaderNewLibrary": "Nouvelle bibliothèque", "HeaderNotifications": "Notifications", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Ouvrir Flux RSS", "HeaderOtherFiles": "Autres fichiers", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Permissions", "HeaderPlayerQueue": "Liste d’écoute", "HeaderPlaylist": "Liste de lecture", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Auteur (Nom, Prénom)", "LabelAuthors": "Auteurs", "LabelAutoDownloadEpisodes": "Téléchargement automatique d’épisode", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Revenir à l’Utilisateur", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Activer les sauvegardes automatiques", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Une seule sauvegarde sera effacée à la fois. Si vous avez plus de sauvegardes à effacer, vous devrez le faire manuellement.", "LabelBitrate": "Bitrate", "LabelBooks": "Livres", + "LabelButtonText": "Button Text", "LabelChangePassword": "Modifier le mot de passe", "LabelChannels": "Canaux", "LabelChapters": "Chapitres", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Lecteur multimédia", "LabelMediaType": "Type de média", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/gu.json b/client/strings/gu.json index d71c9f176..3fca0367e 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise સૂચના સેટિંગ્સ", "HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudioTracks": "Audio Tracks", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Backups", "HeaderChangePassword": "Change Password", "HeaderChapters": "Chapters", @@ -131,8 +132,10 @@ "HeaderNewAccount": "New Account", "HeaderNewLibrary": "New Library", "HeaderNotifications": "Notifications", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Open RSS Feed", "HeaderOtherFiles": "Other Files", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Permissions", "HeaderPlayerQueue": "Player Queue", "HeaderPlaylist": "Playlist", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthors": "Authors", "LabelAutoDownloadEpisodes": "Auto Download Episodes", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Back to User", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", "LabelBitrate": "Bitrate", "LabelBooks": "Books", + "LabelButtonText": "Button Text", "LabelChangePassword": "Change Password", "LabelChannels": "Channels", "LabelChapters": "Chapters", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/hi.json b/client/strings/hi.json index 51b2e7629..1f35e11aa 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise अधिसूचना सेटिंग्स", "HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudioTracks": "Audio Tracks", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Backups", "HeaderChangePassword": "Change Password", "HeaderChapters": "Chapters", @@ -131,8 +132,10 @@ "HeaderNewAccount": "New Account", "HeaderNewLibrary": "New Library", "HeaderNotifications": "Notifications", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Open RSS Feed", "HeaderOtherFiles": "Other Files", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Permissions", "HeaderPlayerQueue": "Player Queue", "HeaderPlaylist": "Playlist", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthors": "Authors", "LabelAutoDownloadEpisodes": "Auto Download Episodes", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Back to User", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", "LabelBitrate": "Bitrate", "LabelBooks": "Books", + "LabelButtonText": "Button Text", "LabelChangePassword": "Change Password", "LabelChannels": "Channels", "LabelChapters": "Chapters", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/hr.json b/client/strings/hr.json index e04343a09..5fe09ab27 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise Notification Settings", "HeaderAudiobookTools": "Audiobook File Management alati", "HeaderAudioTracks": "Audio Tracks", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Backups", "HeaderChangePassword": "Promijeni lozinku", "HeaderChapters": "Poglavlja", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Novi korisnički račun", "HeaderNewLibrary": "Nova biblioteka", "HeaderNotifications": "Obavijesti", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Otvori RSS Feed", "HeaderOtherFiles": "Druge datoteke", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Dozvole", "HeaderPlayerQueue": "Player Queue", "HeaderPlaylist": "Playlist", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthors": "Autori", "LabelAutoDownloadEpisodes": "Automatski preuzmi epizode", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Nazad k korisniku", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Uključi automatski backup", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Samo 1 backup će biti odjednom obrisan. Ako koristite više njih, morati ćete ih ručno ukloniti.", "LabelBitrate": "Bitrate", "LabelBooks": "Knjige", + "LabelButtonText": "Button Text", "LabelChangePassword": "Promijeni lozinku", "LabelChannels": "Channels", "LabelChapters": "Chapters", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/it.json b/client/strings/it.json index c893212e1..881cc19bf 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprendi le impostazioni di Notifica", "HeaderAudiobookTools": "Utilità Audiobook File Management", "HeaderAudioTracks": "Tracce Audio", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Backup", "HeaderChangePassword": "Cambia Password", "HeaderChapters": "Capitoli", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Nuovo Account", "HeaderNewLibrary": "Nuova Libreria", "HeaderNotifications": "Notifiche", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Apri RSS Feed", "HeaderOtherFiles": "Altri File", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Permessi", "HeaderPlayerQueue": "Coda Riproduzione", "HeaderPlaylist": "Playlist", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Autori (Per Cognome)", "LabelAuthors": "Autori", "LabelAutoDownloadEpisodes": "Auto Download Episodi", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Torna a Utenti", "LabelBackupLocation": "Percorso del Backup", "LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Verrà rimosso solo 1 backup alla volta, quindi se hai più backup, dovrai rimuoverli manualmente.", "LabelBitrate": "Bitrate", "LabelBooks": "Libri", + "LabelButtonText": "Button Text", "LabelChangePassword": "Cambia Password", "LabelChannels": "Canali", "LabelChapters": "Capitoli", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Allarme", "LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Tipo Media", "LabelMetadataOrderOfPrecedenceDescription": "1 e bassa priorità, 5 è alta priorità", @@ -726,4 +736,4 @@ "ToastSocketFailedToConnect": "Socket non riesce a connettersi", "ToastUserDeleteFailed": "Errore eliminazione utente", "ToastUserDeleteSuccess": "Utente eliminato" -} +} \ No newline at end of file diff --git a/client/strings/lt.json b/client/strings/lt.json index ebc6b5588..00d3aeeda 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise pranešimo nustatymai", "HeaderAudiobookTools": "Audioknygų failų valdymo įrankiai", "HeaderAudioTracks": "Garso takeliai", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Atsarginės kopijos", "HeaderChangePassword": "Pakeisti slaptažodį", "HeaderChapters": "Skyriai", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Nauja paskyra", "HeaderNewLibrary": "Nauja biblioteka", "HeaderNotifications": "Pranešimai", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Atidaryti RSS srautą", "HeaderOtherFiles": "Kiti failai", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Leidimai", "HeaderPlayerQueue": "Grotuvo eilė", "HeaderPlaylist": "Grojaraštis", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Autorius (Pavardė, Vardas)", "LabelAuthors": "Autoriai", "LabelAutoDownloadEpisodes": "Automatiškai atsisiųsti epizodus", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Grįžti į naudotoją", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Įjungti automatinį atsarginių kopijų kūrimą", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Tik viena atsarginė kopija bus pašalinta vienu metu, todėl jei jau turite daugiau atsarginių kopijų nei nurodyta, turite jas pašalinti rankiniu būdu.", "LabelBitrate": "Bitų sparta", "LabelBooks": "Knygos", + "LabelButtonText": "Button Text", "LabelChangePassword": "Pakeisti slaptažodį", "LabelChannels": "Kanalai", "LabelChapters": "Skyriai", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Ieškoti naujų epizodų po šios datos", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Grotuvas", "LabelMediaType": "Medijos tipas", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/nl.json b/client/strings/nl.json index 06aed9045..1421368be 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen", "HeaderAudiobookTools": "Audioboekbestandbeheer tools", "HeaderAudioTracks": "Audiotracks", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Back-ups", "HeaderChangePassword": "Wachtwoord wijzigen", "HeaderChapters": "Hoofdstukken", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Nieuwe account", "HeaderNewLibrary": "Nieuwe bibliotheek", "HeaderNotifications": "Notificaties", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Open RSS-feed", "HeaderOtherFiles": "Andere bestanden", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Toestemmingen", "HeaderPlayerQueue": "Afspeelwachtrij", "HeaderPlaylist": "Afspeellijst", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Auteur (Achternaam, Voornaam)", "LabelAuthors": "Auteurs", "LabelAutoDownloadEpisodes": "Afleveringen automatisch downloaden", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Terug naar gebruiker", "LabelBackupLocation": "Back-up locatie", "LabelBackupsEnableAutomaticBackups": "Automatische back-ups inschakelen", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Er wordt slechts 1 back-up per keer verwijderd, dus als je reeds meer back-ups dan dit hebt moet je ze handmatig verwijderen.", "LabelBitrate": "Bitrate", "LabelBooks": "Boeken", + "LabelButtonText": "Button Text", "LabelChangePassword": "Wachtwoord wijzigen", "LabelChannels": "Kanalen", "LabelChapters": "Hoofdstukken", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Waarschuwing", "LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Mediaspeler", "LabelMediaType": "Mediatype", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/no.json b/client/strings/no.json index 7fcd1c96a..7d2acf3bc 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise notifikasjonsinstillinger", "HeaderAudiobookTools": "Lydbok Filbehandlingsverktøy", "HeaderAudioTracks": "Lydspor", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Sikkerhetskopier", "HeaderChangePassword": "Bytt passord", "HeaderChapters": "Kapittel", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Ny konto", "HeaderNewLibrary": "Ny bibliotek", "HeaderNotifications": "Notifikasjoner", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Åpne RSS Feed", "HeaderOtherFiles": "Andre filer", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Rettigheter", "HeaderPlayerQueue": "Spiller kø", "HeaderPlaylist": "Spilleliste", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Forfatter (Etternavn Fornavn)", "LabelAuthors": "Forfattere", "LabelAutoDownloadEpisodes": "Last ned episoder automatisk", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Tilbake til bruker", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Aktiver automatisk sikkerhetskopi", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Kun 1 sikkerhetskopi vil bli fjernet om gangen, hvis du allerede har flere sikkerhetskopier enn dette bør du fjerne de manuelt.", "LabelBitrate": "Bithastighet", "LabelBooks": "Bøker", + "LabelButtonText": "Button Text", "LabelChangePassword": "Endre passord", "LabelChannels": "Kanaler", "LabelChapters": "Kapitler", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Se etter nye episoder etter denne datoen", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Mediespiller", "LabelMediaType": "Medie type", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/pl.json b/client/strings/pl.json index dd3c1d4ad..b0521f0a4 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Ustawienia powiadomień Apprise", "HeaderAudiobookTools": "Narzędzia do zarządzania audiobookami", "HeaderAudioTracks": "Ścieżki audio", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Kopie zapasowe", "HeaderChangePassword": "Zmień hasło", "HeaderChapters": "Rozdziały", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Nowe konto", "HeaderNewLibrary": "Nowa biblioteka", "HeaderNotifications": "Powiadomienia", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Utwórz kanał RSS", "HeaderOtherFiles": "Inne pliki", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Uprawnienia", "HeaderPlayerQueue": "Player Queue", "HeaderPlaylist": "Playlist", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Author (Malejąco)", "LabelAuthors": "Autorzy", "LabelAutoDownloadEpisodes": "Automatyczne pobieranie odcinków", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Powrót", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Włącz automatyczne kopie zapasowe", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Tylko 1 kopia zapasowa zostanie usunięta, więc jeśli masz już więcej kopii zapasowych, powinieneś je ręcznie usunąć.", "LabelBitrate": "Bitrate", "LabelBooks": "Książki", + "LabelButtonText": "Button Text", "LabelChangePassword": "Zmień hasło", "LabelChannels": "Channels", "LabelChapters": "Chapters", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Informacja", "LabelLogLevelWarn": "Ostrzeżenie", "LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Odtwarzacz", "LabelMediaType": "Typ mediów", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/ru.json b/client/strings/ru.json index 832ffe8b6..851d2ba31 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Настройки оповещений", "HeaderAudiobookTools": "Инструменты файлов аудиокниг", "HeaderAudioTracks": "Аудио треки", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Бэкапы", "HeaderChangePassword": "Изменить пароль", "HeaderChapters": "Главы", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Новая учетная запись", "HeaderNewLibrary": "Новая библиотека", "HeaderNotifications": "Уведомления", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Открыть RSS-канал", "HeaderOtherFiles": "Другие файлы", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Разрешения", "HeaderPlayerQueue": "Очередь воспроизведения", "HeaderPlaylist": "Плейлист", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Автор (Фамилия, Имя)", "LabelAuthors": "Авторы", "LabelAutoDownloadEpisodes": "Скачивать эпизоды автоматически", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Назад к пользователю", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Включить автоматическое бэкапирование", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "За один раз только 1 бэкап будет удален, так что если у вас будет больше бэкапов, то их нужно удалить вручную.", "LabelBitrate": "Битрейт", "LabelBooks": "Книги", + "LabelButtonText": "Button Text", "LabelChangePassword": "Изменить пароль", "LabelChannels": "Каналы", "LabelChapters": "Главы", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Медиа проигрыватель", "LabelMediaType": "Тип медиа", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", diff --git a/client/strings/sv.json b/client/strings/sv.json index 23d489d05..eea30043d 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise Meddelandeinställningar", "HeaderAudiobookTools": "Ljudbokshantering", "HeaderAudioTracks": "Ljudspår", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Säkerhetskopior", "HeaderChangePassword": "Ändra lösenord", "HeaderChapters": "Kapitel", @@ -131,8 +132,10 @@ "HeaderNewAccount": "Nytt konto", "HeaderNewLibrary": "Nytt bibliotek", "HeaderNotifications": "Meddelanden", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "Öppna RSS-flöde", "HeaderOtherFiles": "Andra filer", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "Behörigheter", "HeaderPlayerQueue": "Spelarkö", "HeaderPlaylist": "Spellista", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "Författare (Efternamn, Förnamn)", "LabelAuthors": "Författare", "LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Tillbaka till användaren", "LabelBackupLocation": "Säkerhetskopia Plats", "LabelBackupsEnableAutomaticBackups": "Aktivera automatiska säkerhetskopior", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "Endast en säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än detta bör du ta bort dem manuellt.", "LabelBitrate": "Bitfrekvens", "LabelBooks": "Böcker", + "LabelButtonText": "Button Text", "LabelChangePassword": "Ändra lösenord", "LabelChannels": "Kanaler", "LabelChapters": "Kapitel", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "Felsökningsnivå: Information", "LabelLogLevelWarn": "Felsökningsnivå: Varning", "LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Mediaspelare", "LabelMediaType": "Mediatyp", "LabelMetadataOrderOfPrecedenceDescription": "1 är lägsta prioritet, 5 är högsta prioritet", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 5d3de27ad..8bb242a4f 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "测试通知设置", "HeaderAudiobookTools": "有声读物文件管理工具", "HeaderAudioTracks": "音轨", + "HeaderAuthentication": "Authentication", "HeaderBackups": "备份", "HeaderChangePassword": "更改密码", "HeaderChapters": "章节", @@ -131,8 +132,10 @@ "HeaderNewAccount": "新建帐户", "HeaderNewLibrary": "新建媒体库", "HeaderNotifications": "通知", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenRSSFeed": "打开 RSS 源", "HeaderOtherFiles": "其他文件", + "HeaderPasswordAuthentication": "Password Authentication", "HeaderPermissions": "权限", "HeaderPlayerQueue": "播放队列", "HeaderPlaylist": "播放列表", @@ -193,6 +196,10 @@ "LabelAuthorLastFirst": "作者 (名, 姓)", "LabelAuthors": "作者", "LabelAutoDownloadEpisodes": "自动下载剧集", + "LabelAutoLaunch": "Auto Launch", + "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Register", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "返回到用户", "LabelBackupLocation": "备份位置", "LabelBackupsEnableAutomaticBackups": "启用自动备份", @@ -203,6 +210,7 @@ "LabelBackupsNumberToKeepHelp": "一次只能删除一个备份, 因此如果你已经有超过此数量的备份, 则应手动删除它们.", "LabelBitrate": "比特率", "LabelBooks": "图书", + "LabelButtonText": "Button Text", "LabelChangePassword": "修改密码", "LabelChannels": "声道", "LabelChapters": "章节", @@ -316,6 +324,8 @@ "LabelLogLevelInfo": "信息", "LabelLogLevelWarn": "警告", "LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集", + "LabelMatchExistingUsersBy": "Match existing users by", + "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "媒体播放器", "LabelMediaType": "媒体类型", "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", From 5e1e748c71d70ad2638405cec0667f84f8beee40 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 23 Nov 2023 09:53:52 +0200 Subject: [PATCH 173/208] Add ApiCacheManager unit test --- server/managers/ApiCacheManager | 19 +++-- test/server/managers/ApiCacheManager.test.js | 85 ++++++++++++++++++++ 2 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 test/server/managers/ApiCacheManager.test.js diff --git a/server/managers/ApiCacheManager b/server/managers/ApiCacheManager index 882b9b61a..b311af53d 100644 --- a/server/managers/ApiCacheManager +++ b/server/managers/ApiCacheManager @@ -3,12 +3,16 @@ const Logger = require('../Logger') const Database = require('../Database') class ApiCacheManager { - constructor(options = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: item => item.length }) { - this.options = options + + defaultCacheOptions = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: item => item.length } + defaultTtlOptions = { ttl: 30 * 60 * 1000 } + + constructor(cache = new LRUCache(this.defaultCacheOptions), ttlOptions = this.defaultTtlOptions) { + this.cache = cache + this.ttlOptions = ttlOptions } init(database = Database) { - this.cache = new LRUCache(this.options) let hooks = ['afterCreate', 'afterUpdate', 'afterDestroy', 'afterBulkCreate', 'afterBulkUpdate', 'afterBulkDestroy'] hooks.forEach(hook => database.sequelize.addHook(hook, (model) => this.clear(model, hook))) } @@ -23,23 +27,22 @@ class ApiCacheManager { const key = { user: req.user.username, url: req.url } const stringifiedKey = JSON.stringify(key) Logger.debug(`[ApiCacheManager] count: ${this.cache.size} size: ${this.cache.calculatedSize}`) - Logger.debug(`[ApiCacheManager] Cache key: ${stringifiedKey}`) const cached = this.cache.get(stringifiedKey) if (cached) { Logger.debug(`[ApiCacheManager] Cache hit: ${stringifiedKey}`) res.send(cached) return } - res.sendResponse = res.send + res.originalSend = res.send res.send = (body) => { Logger.debug(`[ApiCacheManager] Cache miss: ${stringifiedKey}`) if (key.url.search(/^\/libraries\/.*?\/personalized/) !== -1) { - Logger.debug(`[ApiCacheManager] Caching personalized with 30 minues TTL`) - this.cache.set(stringifiedKey, body, { ttl: 30 * 60 * 1000 }) + Logger.debug(`[ApiCacheManager] Caching with ${this.ttlOptions.ttl} ms TTL`) + this.cache.set(stringifiedKey, body, this.ttlOptions) } else { this.cache.set(stringifiedKey, body) } - res.sendResponse(body) + res.originalSend(body) } next() } diff --git a/test/server/managers/ApiCacheManager.test.js b/test/server/managers/ApiCacheManager.test.js new file mode 100644 index 000000000..2cfc0dfcf --- /dev/null +++ b/test/server/managers/ApiCacheManager.test.js @@ -0,0 +1,85 @@ +// Import dependencies and modules for testing +const { expect } = require('chai') +const sinon = require('sinon') +const ApiCacheManager = require('../../../server/managers/ApiCacheManager') + +describe('ApiCacheManager', () => { + let cache + let req + let res + let next + let manager + + beforeEach(() => { + cache = { get: sinon.stub(), set: sinon.spy() } + req = { user: { username: 'testUser' }, url: '/test-url' } + res = { send: sinon.spy() } + next = sinon.spy() + }) + + describe('middleware', () => { + it('should send cached data if available', () => { + // Arrange + const cachedData = { data: 'cached data' } + cache.get.returns(cachedData) + const key = JSON.stringify({ user: req.user.username, url: req.url }) + manager = new ApiCacheManager(cache) + + // Act + manager.middleware(req, res, next) + + // Assert + expect(cache.get.calledOnce).to.be.true + expect(cache.get.calledWith(key)).to.be.true + expect(res.send.calledOnce).to.be.true + expect(res.send.calledWith(cachedData)).to.be.true + expect(res.originalSend).to.be.undefined + expect(next.called).to.be.false + expect(cache.set.called).to.be.false + }) + + it('should cache and send response if data is not cached', () => { + // Arrange + cache.get.returns(null) + const responseData = { data: 'response data' } + const key = JSON.stringify({ user: req.user.username, url: req.url }) + manager = new ApiCacheManager(cache) + + // Act + manager.middleware(req, res, next) + res.send(responseData) + + // Assert + expect(cache.get.calledOnce).to.be.true + expect(cache.get.calledWith(key)).to.be.true + expect(next.calledOnce).to.be.true + expect(cache.set.calledOnce).to.be.true + expect(cache.set.calledWith(key, responseData)).to.be.true + expect(res.originalSend.calledOnce).to.be.true + expect(res.originalSend.calledWith(responseData)).to.be.true + }) + + it('should cache personalized response with 30 minutes TTL', () => { + // Arrange + cache.get.returns(null) + const responseData = { data: 'personalized data' } + req.url = '/libraries/id/personalized' + const key = JSON.stringify({ user: req.user.username, url: req.url }) + const ttlOptions = { ttl: 30 * 60 * 1000 } + manager = new ApiCacheManager(cache, ttlOptions) + + // Act + manager.middleware(req, res, next) + res.send(responseData) + + // Assert + expect(cache.get.calledOnce).to.be.true + expect(cache.get.calledWith(key)).to.be.true + expect(next.calledOnce).to.be.true + expect(cache.set.calledOnce).to.be.true + expect(cache.set.calledWith(key, responseData, ttlOptions)).to.be.true + expect(res.originalSend.calledOnce).to.be.true + expect(res.originalSend.calledWith(responseData)).to.be.true + }) + }) +}) \ No newline at end of file From 07d7d164187b5b3546a8f4b50ad814f83393781b Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 23 Nov 2023 09:55:55 +0200 Subject: [PATCH 174/208] Use a single router.get for API cache middleware --- server/routers/ApiRouter.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index b18882951..d77145681 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -32,7 +32,6 @@ const MiscController = require('../controllers/MiscController') const Author = require('../objects/entities/Author') const Series = require('../objects/entities/Series') -const { measureMiddleware } = require('../utils/timing') class ApiRouter { constructor(Server) { @@ -57,32 +56,32 @@ class ApiRouter { } init() { - const cacheMiddleware = this.apiCacheManager.middleware // // Library Routes // + this.router.get(/^\/libraries/, this.apiCacheManager.middleware) this.router.post('/libraries', LibraryController.create.bind(this)) this.router.get('/libraries', LibraryController.findAll.bind(this)) - this.router.get('/libraries/:id', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.findOne.bind(this)) + this.router.get('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.findOne.bind(this)) this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this)) this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this)) - this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getLibraryItems.bind(this)) + this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this)) this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this)) - this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getEpisodeDownloadQueue.bind(this)) - this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getAllSeriesForLibrary.bind(this)) + this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this)) + this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this)) this.router.get('/libraries/:id/series/:seriesId', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this)) - this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getCollectionsForLibrary.bind(this)) - this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getUserPlaylistsForLibrary.bind(this)) - this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getUserPersonalizedShelves.bind(this)) - this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getLibraryFilterData.bind(this)) - this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.search.bind(this)) + this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this)) + this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this)) + this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getUserPersonalizedShelves.bind(this)) + this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this)) + this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this)) this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this)) - this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getAuthors.bind(this)) - this.router.get('/libraries/:id/narrators', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.getNarrators.bind(this)) + this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this)) + this.router.get('/libraries/:id/narrators', LibraryController.middleware.bind(this), LibraryController.getNarrators.bind(this)) this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.updateNarrator.bind(this)) this.router.delete('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.removeNarrator.bind(this)) - this.router.get('/libraries/:id/matchall', LibraryController.middleware.bind(this), cacheMiddleware, LibraryController.matchAll.bind(this)) + this.router.get('/libraries/:id/matchall', LibraryController.middleware.bind(this), LibraryController.matchAll.bind(this)) this.router.post('/libraries/:id/scan', LibraryController.middleware.bind(this), LibraryController.scan.bind(this)) this.router.get('/libraries/:id/recent-episodes', LibraryController.middleware.bind(this), LibraryController.getRecentEpisodes.bind(this)) this.router.get('/libraries/:id/opml', LibraryController.middleware.bind(this), LibraryController.getOPMLFile.bind(this)) From ab19e25586f0e5c8fe7e65d741971a70aea16187 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 23 Nov 2023 09:56:37 +0200 Subject: [PATCH 175/208] Remove unnecessary timing measurements --- server/Server.js | 3 --- server/utils/timing.js | 1 - 2 files changed, 4 deletions(-) diff --git a/server/Server.js b/server/Server.js index 9ccd829b1..77c004e8e 100644 --- a/server/Server.js +++ b/server/Server.js @@ -39,9 +39,6 @@ const LibraryScanner = require('./scanner/LibraryScanner') const passport = require('passport') const expressSession = require('express-session') -const { measureMiddleware } = require('./utils/timing') - - class Server { constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { this.Port = PORT diff --git a/server/utils/timing.js b/server/utils/timing.js index af019e78b..6945a706e 100644 --- a/server/utils/timing.js +++ b/server/utils/timing.js @@ -1,7 +1,6 @@ const { performance } = require('perf_hooks') const Logger = require('../Logger') - async function measure(tag, func) { const start = performance.now() const result = await func() From 9beee3ed657cc6ea3b7dbdb1a667a7af6c82ae53 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 23 Nov 2023 15:14:49 -0600 Subject: [PATCH 176/208] Fix:Change password api endpoint --- server/Auth.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/server/Auth.js b/server/Auth.js index e2053fa56..dedf32f01 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -566,6 +566,69 @@ class Auth { Source: global.Source } } + + /** + * + * @param {string} password + * @param {*} user + * @returns {boolean} + */ + comparePassword(password, user) { + if (user.type === 'root' && !password && !user.pash) return true + if (!password || !user.pash) return false + return bcrypt.compare(password, user.pash) + } + + /** + * User changes their password from request + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async userChangePassword(req, res) { + let { password, newPassword } = req.body + newPassword = newPassword || '' + const matchingUser = req.user + + // Only root can have an empty password + if (matchingUser.type !== 'root' && !newPassword) { + return res.json({ + error: 'Invalid new password - Only root can have an empty password' + }) + } + + // Check password match + const compare = await this.comparePassword(password, matchingUser) + if (!compare) { + return res.json({ + error: 'Invalid password' + }) + } + + let pw = '' + if (newPassword) { + pw = await this.hashPass(newPassword) + if (!pw) { + return res.json({ + error: 'Hash failed' + }) + } + } + + matchingUser.pash = pw + + const success = await Database.updateUser(matchingUser) + if (success) { + Logger.info(`[Auth] User "${matchingUser.username}" changed password`) + res.json({ + success: true + }) + } else { + res.json({ + error: 'Unknown error' + }) + } + } } module.exports = Auth \ No newline at end of file From 572fb0993cb548d662ceefb3e4cbcf85f1e84bd7 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 24 Nov 2023 14:20:14 -0600 Subject: [PATCH 177/208] Rename ApiCacheManager to add .js file extension --- server/managers/{ApiCacheManager => ApiCacheManager.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename server/managers/{ApiCacheManager => ApiCacheManager.js} (100%) diff --git a/server/managers/ApiCacheManager b/server/managers/ApiCacheManager.js similarity index 100% rename from server/managers/ApiCacheManager rename to server/managers/ApiCacheManager.js From 7a9c869ac5163968120bfd28b72f397d7367bdf7 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 24 Nov 2023 14:27:32 -0600 Subject: [PATCH 178/208] Ignore sequelize hooks when updating user lastSeen on socket authentication --- server/SocketAuthority.js | 4 ++-- server/models/User.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 310121077..da17f5df1 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -192,9 +192,9 @@ class SocketAuthority { this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions)) - // Update user lastSeen + // Update user lastSeen without firing sequelize bulk update hooks user.lastSeen = Date.now() - await Database.updateUser(user) + await Database.userModel.updateFromOld(user, false) const initialPayload = { userId: client.user.id, diff --git a/server/models/User.js b/server/models/User.js index 4c348f42f..220c0c406 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -99,11 +99,13 @@ class User extends Model { * Update User from old user model * * @param {oldUser} oldUser + * @param {boolean} [hooks=true] Run before / after bulk update hooks? * @returns {Promise<boolean>} */ - static updateFromOld(oldUser) { + static updateFromOld(oldUser, hooks = true) { const user = this.getFromOld(oldUser) return this.update(user, { + hooks: !!hooks, where: { id: user.id } From 9d257ebecd2995dfcd9cc0ce17168c453da9fde5 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 24 Nov 2023 15:36:42 -0600 Subject: [PATCH 179/208] Update:Home page shelf bulk items added socket event only adds new items to the recently added shelf instead of refreshing all shelves #2323 --- client/components/app/BookShelfCategorized.vue | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index fbed11be8..15f34867b 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -338,9 +338,15 @@ export default { libraryItemsAdded(libraryItems) { console.log('libraryItems added', libraryItems) - const isThisLibrary = !libraryItems.some((li) => li.libraryId !== this.currentLibraryId) - if (!this.search && isThisLibrary) { - this.fetchCategories() + const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added') + if (!recentlyAddedShelf) return + + // Add new library item to the recently added shelf + for (const libraryItem of libraryItems) { + if (libraryItem.libraryId === this.currentLibraryId && !recentlyAddedShelf.entities.some((ent) => ent.id === libraryItem.id)) { + // Add to front of array + recentlyAddedShelf.entities.unshift(libraryItem) + } } }, libraryItemsUpdated(items) { From 26fc3a196632bb544e17994a4f95081454d21045 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sat, 25 Nov 2023 08:14:45 +0200 Subject: [PATCH 180/208] Remove currently unused time measurement utils --- server/utils/timing.js | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 server/utils/timing.js diff --git a/server/utils/timing.js b/server/utils/timing.js deleted file mode 100644 index 6945a706e..000000000 --- a/server/utils/timing.js +++ /dev/null @@ -1,26 +0,0 @@ -const { performance } = require('perf_hooks') -const Logger = require('../Logger') - -async function measure(tag, func) { - const start = performance.now() - const result = await func() - const end = performance.now() - Logger.debug(`[${tag}] Time elapsed: ${(end - start) | 0} ms`) - return result -} - -function measureMiddleware(req, res, next) { - const start = performance.now() - res.on('finish', () => { - const end = performance.now() - if (!req.originalUrl.includes('cover')) - Logger.debug(`[${req.method} ${req.originalUrl}] Finish: Time elapsed: ${(end - start) | 0} ms`) - }) - res.on('close', () => { - const end = performance.now() - if (!req.originalUrl.includes('cover')) - Logger.debug(`[${req.method} ${req.originalUrl}] Close: Time elapsed: ${(end - start) | 0} ms`) - }) - next() -} -module.exports = { measure, measureMiddleware } \ No newline at end of file From 0fac9e367d927a66400af53949a87c3a08d15cb5 Mon Sep 17 00:00:00 2001 From: JBlond <leet31337@web.de> Date: Sat, 25 Nov 2023 19:10:26 +0100 Subject: [PATCH 181/208] de translation follow up for 2e06ae01a1ec4f670d51eaf8a7a3e5a5b256bdf0 --- client/strings/de.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index fcb1cd33b..6e86b3ac2 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -92,7 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen", "HeaderAudiobookTools": "Hörbuch-Dateiverwaltungstools", "HeaderAudioTracks": "Audiodateien", - "HeaderAuthentication": "Authentication", + "HeaderAuthentication": "Authentifizierung", "HeaderBackups": "Sicherungen", "HeaderChangePassword": "Passwort ändern", "HeaderChapters": "Kapitel", @@ -132,10 +132,10 @@ "HeaderNewAccount": "Neues Konto", "HeaderNewLibrary": "Neue Bibliothek", "HeaderNotifications": "Benachrichtigungen", - "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung", "HeaderOpenRSSFeed": "RSS-Feed öffnen", "HeaderOtherFiles": "Sonstige Dateien", - "HeaderPasswordAuthentication": "Password Authentication", + "HeaderPasswordAuthentication": "Password Authentifizierung", "HeaderPermissions": "Berechtigungen", "HeaderPlayerQueue": "Spieler Warteschlange", "HeaderPlaylist": "Wiedergabeliste", @@ -196,10 +196,10 @@ "LabelAuthorLastFirst": "Autor (Nachname, Vorname)", "LabelAuthors": "Autoren", "LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen", - "LabelAutoLaunch": "Auto Launch", - "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", - "LabelAutoRegister": "Auto Register", - "LabelAutoRegisterDescription": "Automatically create new users after logging in", + "LabelAutoLaunch": "Automatischer Start", + "LabelAutoLaunchDescription": "Automatische Weiterleitung zum Authentifizierungsanbieter beim Navigieren zur Anmeldeseite (manueller Überschreibungspfad <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Automatische Registrierung", + "LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Einloggen", "LabelBackToUser": "Zurück zum Benutzer", "LabelBackupLocation": "Backup-Ort", "LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren", @@ -324,11 +324,11 @@ "LabelLogLevelInfo": "Informationen", "LabelLogLevelWarn": "Warnungen", "LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum", - "LabelMatchExistingUsersBy": "Match existing users by", - "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", + "LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit", + "LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID von Ihrem SSO-Anbieter zugeordnet", "LabelMediaPlayer": "Mediaplayer", "LabelMediaType": "Medientyp", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "1 ist die niedrigste Priorität, 5 ist die höhste Priorität", "LabelMetadataProvider": "Metadatenanbieter", "LabelMetaTag": "Meta Schlagwort", "LabelMetaTags": "Meta Tags", From 3ff41f2b43a89d4f8365a1002647795c983735f6 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sat, 25 Nov 2023 23:49:56 +0200 Subject: [PATCH 182/208] Cache HTTP headers and status --- server/managers/ApiCacheManager.js | 11 ++++--- test/server/managers/ApiCacheManager.test.js | 30 ++++++++++++++------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/server/managers/ApiCacheManager.js b/server/managers/ApiCacheManager.js index b311af53d..c6579ab34 100644 --- a/server/managers/ApiCacheManager.js +++ b/server/managers/ApiCacheManager.js @@ -4,7 +4,7 @@ const Database = require('../Database') class ApiCacheManager { - defaultCacheOptions = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: item => item.length } + defaultCacheOptions = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: item => (item.body.length + JSON.stringify(item.headers).length) } defaultTtlOptions = { ttl: 30 * 60 * 1000 } constructor(cache = new LRUCache(this.defaultCacheOptions), ttlOptions = this.defaultTtlOptions) { @@ -30,17 +30,20 @@ class ApiCacheManager { const cached = this.cache.get(stringifiedKey) if (cached) { Logger.debug(`[ApiCacheManager] Cache hit: ${stringifiedKey}`) - res.send(cached) + res.set(cached.headers) + res.status(cached.statusCode) + res.send(cached.body) return } res.originalSend = res.send res.send = (body) => { Logger.debug(`[ApiCacheManager] Cache miss: ${stringifiedKey}`) + const cached = { body, headers: res.getHeaders(), statusCode: res.statusCode } if (key.url.search(/^\/libraries\/.*?\/personalized/) !== -1) { Logger.debug(`[ApiCacheManager] Caching with ${this.ttlOptions.ttl} ms TTL`) - this.cache.set(stringifiedKey, body, this.ttlOptions) + this.cache.set(stringifiedKey, cached, this.ttlOptions) } else { - this.cache.set(stringifiedKey, body) + this.cache.set(stringifiedKey, cached) } res.originalSend(body) } diff --git a/test/server/managers/ApiCacheManager.test.js b/test/server/managers/ApiCacheManager.test.js index 2cfc0dfcf..dc1ee1ed4 100644 --- a/test/server/managers/ApiCacheManager.test.js +++ b/test/server/managers/ApiCacheManager.test.js @@ -13,14 +13,14 @@ describe('ApiCacheManager', () => { beforeEach(() => { cache = { get: sinon.stub(), set: sinon.spy() } req = { user: { username: 'testUser' }, url: '/test-url' } - res = { send: sinon.spy() } + res = { send: sinon.spy(), getHeaders: sinon.stub(), statusCode: 200, status: sinon.spy(), set: sinon.spy() } next = sinon.spy() }) describe('middleware', () => { it('should send cached data if available', () => { // Arrange - const cachedData = { data: 'cached data' } + const cachedData = { body: 'cached data', headers: { 'content-type': 'application/json' }, statusCode: 200 } cache.get.returns(cachedData) const key = JSON.stringify({ user: req.user.username, url: req.url }) manager = new ApiCacheManager(cache) @@ -31,8 +31,12 @@ describe('ApiCacheManager', () => { // Assert expect(cache.get.calledOnce).to.be.true expect(cache.get.calledWith(key)).to.be.true + expect(res.set.calledOnce).to.be.true + expect(res.set.calledWith(cachedData.headers)).to.be.true + expect(res.status.calledOnce).to.be.true + expect(res.status.calledWith(cachedData.statusCode)).to.be.true expect(res.send.calledOnce).to.be.true - expect(res.send.calledWith(cachedData)).to.be.true + expect(res.send.calledWith(cachedData.body)).to.be.true expect(res.originalSend).to.be.undefined expect(next.called).to.be.false expect(cache.set.called).to.be.false @@ -41,13 +45,17 @@ describe('ApiCacheManager', () => { it('should cache and send response if data is not cached', () => { // Arrange cache.get.returns(null) - const responseData = { data: 'response data' } + const headers = { 'content-type': 'application/json' } + res.getHeaders.returns(headers) + const body = 'response data' + const statusCode = 200 + const responseData = { body, headers, statusCode } const key = JSON.stringify({ user: req.user.username, url: req.url }) manager = new ApiCacheManager(cache) // Act manager.middleware(req, res, next) - res.send(responseData) + res.send(body) // Assert expect(cache.get.calledOnce).to.be.true @@ -56,13 +64,17 @@ describe('ApiCacheManager', () => { expect(cache.set.calledOnce).to.be.true expect(cache.set.calledWith(key, responseData)).to.be.true expect(res.originalSend.calledOnce).to.be.true - expect(res.originalSend.calledWith(responseData)).to.be.true + expect(res.originalSend.calledWith(body)).to.be.true }) it('should cache personalized response with 30 minutes TTL', () => { // Arrange cache.get.returns(null) - const responseData = { data: 'personalized data' } + const headers = { 'content-type': 'application/json' } + res.getHeaders.returns(headers) + const body = 'personalized data' + const statusCode = 200 + const responseData = { body, headers, statusCode } req.url = '/libraries/id/personalized' const key = JSON.stringify({ user: req.user.username, url: req.url }) const ttlOptions = { ttl: 30 * 60 * 1000 } @@ -70,7 +82,7 @@ describe('ApiCacheManager', () => { // Act manager.middleware(req, res, next) - res.send(responseData) + res.send(body) // Assert expect(cache.get.calledOnce).to.be.true @@ -79,7 +91,7 @@ describe('ApiCacheManager', () => { expect(cache.set.calledOnce).to.be.true expect(cache.set.calledWith(key, responseData, ttlOptions)).to.be.true expect(res.originalSend.calledOnce).to.be.true - expect(res.originalSend.calledWith(responseData)).to.be.true + expect(res.originalSend.calledWith(body)).to.be.true }) }) }) \ No newline at end of file From 5e69b54eb0ee39f118221a077802b093ea190b30 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 26 Nov 2023 13:45:43 -0600 Subject: [PATCH 183/208] Reverse order of metadata precedence in UI, add translations --- .../libraries/LibraryScannerSettings.vue | 27 +++++++++++++++---- client/strings/cs.json | 7 +++-- client/strings/da.json | 5 +++- client/strings/de.json | 5 +++- client/strings/en-us.json | 4 ++- client/strings/es.json | 5 +++- client/strings/fr.json | 5 +++- client/strings/gu.json | 5 +++- client/strings/hi.json | 5 +++- client/strings/hr.json | 5 +++- client/strings/it.json | 7 +++-- client/strings/lt.json | 5 +++- client/strings/nl.json | 5 +++- client/strings/no.json | 5 +++- client/strings/pl.json | 5 +++- client/strings/ru.json | 5 +++- client/strings/sv.json | 5 +++- client/strings/zh-cn.json | 5 +++- 18 files changed, 91 insertions(+), 24 deletions(-) diff --git a/client/components/modals/libraries/LibraryScannerSettings.vue b/client/components/modals/libraries/LibraryScannerSettings.vue index 253d4e6b0..8ec73dd03 100644 --- a/client/components/modals/libraries/LibraryScannerSettings.vue +++ b/client/components/modals/libraries/LibraryScannerSettings.vue @@ -19,9 +19,11 @@ <li v-for="(source, index) in metadataSourceMapped" :key="source.id" :class="source.include ? 'item' : 'opacity-50'" class="w-full px-2 flex items-center relative border border-white/10"> <span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 mr-2 md:mr-4">reorder</span> <div class="text-center py-1 w-8 min-w-8"> - {{ source.include ? index + 1 : '' }} + {{ source.include ? getSourceIndex(source.id) : '' }} + </div> + <div class="flex-grow inline-flex justify-between px-4 py-3"> + {{ source.name }} <span v-if="source.include && (index === firstActiveSourceIndex || index === lastActiveSourceIndex)" class="px-2 italic font-semibold text-xs text-gray-400">{{ index === firstActiveSourceIndex ? $strings.LabelHighestPriority : $strings.LabelLowestPriority }}</span> </div> - <div class="flex-grow px-4 py-3">{{ source.name }}</div> <div class="px-2 opacity-100"> <ui-toggle-switch v-model="source.include" :off-color="'error'" @input="includeToggled(source)" /> </div> @@ -97,20 +99,34 @@ export default { }, isBookLibrary() { return this.mediaType === 'book' + }, + firstActiveSourceIndex() { + return this.metadataSourceMapped.findIndex((source) => source.include) + }, + lastActiveSourceIndex() { + return this.metadataSourceMapped.findLastIndex((source) => source.include) } }, methods: { + getSourceIndex(source) { + const activeSources = (this.librarySettings.metadataPrecedence || []).map((s) => s).reverse() + return activeSources.findIndex((s) => s === source) + 1 + }, resetToDefault() { this.metadataSourceMapped = [] for (const key in this.metadataSourceData) { this.metadataSourceMapped.push({ ...this.metadataSourceData[key] }) } + this.metadataSourceMapped.reverse() + this.$emit('update', this.getLibraryData()) }, getLibraryData() { + const metadataSourceIds = this.metadataSourceMapped.map((source) => (source.include ? source.id : null)).filter((s) => s) + metadataSourceIds.reverse() return { settings: { - metadataPrecedence: this.metadataSourceMapped.map((source) => (source.include ? source.id : null)).filter((s) => s) + metadataPrecedence: metadataSourceIds } } }, @@ -125,15 +141,16 @@ export default { }, init() { const metadataPrecedence = this.librarySettings.metadataPrecedence || [] - this.metadataSourceMapped = metadataPrecedence.map((source) => this.metadataSourceData[source]).filter((s) => s) for (const sourceKey in this.metadataSourceData) { if (!metadataPrecedence.includes(sourceKey)) { const unusedSourceData = { ...this.metadataSourceData[sourceKey], include: false } - this.metadataSourceMapped.push(unusedSourceData) + this.metadataSourceMapped.unshift(unusedSourceData) } } + + this.metadataSourceMapped.reverse() } }, mounted() { diff --git a/client/strings/cs.json b/client/strings/cs.json index 07f3d4f7d..71d06dca8 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise", "HeaderAudiobookTools": "Nástroje pro správu souborů audioknih", "HeaderAudioTracks": "Zvukové stopy", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Zálohy", "HeaderChangePassword": "Změnit heslo", "HeaderChapters": "Kapitoly", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Trvale smazat soubor", "LabelHasEbook": "Obsahuje elektronickou knihu", "LabelHasSupplementaryEbook": "Obsahuje doplňkovou elektronickou knihu", + "LabelHighestPriority": "Highest priority", "LabelHost": "Hostitel", "LabelHour": "Hodina", "LabelIcon": "Ikona", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Informace", "LabelLogLevelWarn": "Varovat", "LabelLookForNewEpisodesAfterDate": "Hledat nové epizody po tomto datu", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Přehrávač médií", "LabelMediaType": "Typ média", - "LabelMetadataOrderOfPrecedenceDescription": "1 je nejnižší priorita, 5 je nejvyšší priorita", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Poskytovatel metadat", "LabelMetaTag": "Metaznačka", "LabelMetaTags": "Metaznačky", @@ -726,4 +729,4 @@ "ToastSocketFailedToConnect": "Socket se nepodařilo připojit", "ToastUserDeleteFailed": "Nepodařilo se smazat uživatele", "ToastUserDeleteSuccess": "Uživatel smazán" -} +} \ No newline at end of file diff --git a/client/strings/da.json b/client/strings/da.json index 768bb724f..3fafae9f5 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger", "HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer", "HeaderAudioTracks": "Lydspor", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Sikkerhedskopier", "HeaderChangePassword": "Skift Adgangskode", "HeaderChapters": "Kapitler", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Permanent slet fil", "LabelHasEbook": "Har e-bog", "LabelHasSupplementaryEbook": "Har supplerende e-bog", + "LabelHighestPriority": "Highest priority", "LabelHost": "Vært", "LabelHour": "Time", "LabelIcon": "Ikon", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Information", "LabelLogLevelWarn": "Advarsel", "LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Medieafspiller", "LabelMediaType": "Medietype", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadataudbyder", "LabelMetaTag": "Meta-tag", "LabelMetaTags": "Meta-tags", diff --git a/client/strings/de.json b/client/strings/de.json index f7cf8b68e..f957ca99a 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen", "HeaderAudiobookTools": "Hörbuch-Dateiverwaltungstools", "HeaderAudioTracks": "Audiodateien", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Sicherungen", "HeaderChangePassword": "Passwort ändern", "HeaderChapters": "Kapitel", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Datei dauerhaft löschen", "LabelHasEbook": "mit E-Book", "LabelHasSupplementaryEbook": "mit zusätlichem E-Book", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Stunde", "LabelIcon": "Symbol", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Informationen", "LabelLogLevelWarn": "Warnungen", "LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Mediaplayer", "LabelMediaType": "Medientyp", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadatenanbieter", "LabelMetaTag": "Meta Schlagwort", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 6f06ca77e..8d9a22c8c 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -276,6 +276,7 @@ "LabelHardDeleteFile": "Hard delete file", "LabelHasEbook": "Has ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", @@ -317,9 +318,10 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/es.json b/client/strings/es.json index 0ac0a9601..d4752c1a4 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Ajustes de Notificaciones de Apprise", "HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro", "HeaderAudioTracks": "Pistas de Audio", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Respaldos", "HeaderChangePassword": "Cambiar Contraseña", "HeaderChapters": "Capítulos", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Eliminar Definitivamente", "LabelHasEbook": "Tiene Ebook", "LabelHasSupplementaryEbook": "Tiene Ebook Suplementario", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Hora", "LabelIcon": "Icono", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Información", "LabelLogLevelWarn": "Advertencia", "LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Reproductor de Medios", "LabelMediaType": "Tipo de Multimedia", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Proveedor de Metadata", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/fr.json b/client/strings/fr.json index 5ad80723f..56f214a29 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise", "HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook", "HeaderAudioTracks": "Pistes audio", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Sauvegardes", "HeaderChangePassword": "Modifier le mot de passe", "HeaderChapters": "Chapitres", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Suppression du fichier", "LabelHasEbook": "Dispose d’un livre numérique", "LabelHasSupplementaryEbook": "Dispose d’un livre numérique supplémentaire", + "LabelHighestPriority": "Highest priority", "LabelHost": "Hôte", "LabelHour": "Heure", "LabelIcon": "Icone", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Lecteur multimédia", "LabelMediaType": "Type de média", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Fournisseur de métadonnées", "LabelMetaTag": "Etiquette de métadonnée", "LabelMetaTags": "Etiquettes de métadonnée", diff --git a/client/strings/gu.json b/client/strings/gu.json index d71c9f176..6e11a221b 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise સૂચના સેટિંગ્સ", "HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudioTracks": "Audio Tracks", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Backups", "HeaderChangePassword": "Change Password", "HeaderChapters": "Chapters", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Hard delete file", "LabelHasEbook": "Has ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/hi.json b/client/strings/hi.json index 51b2e7629..6e228c899 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise अधिसूचना सेटिंग्स", "HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudioTracks": "Audio Tracks", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Backups", "HeaderChangePassword": "Change Password", "HeaderChapters": "Chapters", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Hard delete file", "LabelHasEbook": "Has ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/hr.json b/client/strings/hr.json index e04343a09..6c24b1bd3 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise Notification Settings", "HeaderAudiobookTools": "Audiobook File Management alati", "HeaderAudioTracks": "Audio Tracks", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Backups", "HeaderChangePassword": "Promijeni lozinku", "HeaderChapters": "Poglavlja", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Obriši datoteku zauvijek", "LabelHasEbook": "Has ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Sat", "LabelIcon": "Ikona", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Poslužitelj metapodataka ", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/it.json b/client/strings/it.json index c893212e1..d5cee73cb 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprendi le impostazioni di Notifica", "HeaderAudiobookTools": "Utilità Audiobook File Management", "HeaderAudioTracks": "Tracce Audio", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Backup", "HeaderChangePassword": "Cambia Password", "HeaderChapters": "Capitoli", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Elimina Definitivamente", "LabelHasEbook": "Un ebook", "LabelHasSupplementaryEbook": "Un ebook Supplementare", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Ora", "LabelIcon": "Icona", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Allarme", "LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Tipo Media", - "LabelMetadataOrderOfPrecedenceDescription": "1 e bassa priorità, 5 è alta priorità", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", @@ -726,4 +729,4 @@ "ToastSocketFailedToConnect": "Socket non riesce a connettersi", "ToastUserDeleteFailed": "Errore eliminazione utente", "ToastUserDeleteSuccess": "Utente eliminato" -} +} \ No newline at end of file diff --git a/client/strings/lt.json b/client/strings/lt.json index ebc6b5588..520aaa747 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise pranešimo nustatymai", "HeaderAudiobookTools": "Audioknygų failų valdymo įrankiai", "HeaderAudioTracks": "Garso takeliai", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Atsarginės kopijos", "HeaderChangePassword": "Pakeisti slaptažodį", "HeaderChapters": "Skyriai", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Galutinai ištrinti failą", "LabelHasEbook": "Turi e-knygą", "LabelHasSupplementaryEbook": "Turi papildomą e-knygą", + "LabelHighestPriority": "Highest priority", "LabelHost": "Serveris", "LabelHour": "Valanda", "LabelIcon": "Piktograma", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Ieškoti naujų epizodų po šios datos", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Grotuvas", "LabelMediaType": "Medijos tipas", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metaduomenų tiekėjas", "LabelMetaTag": "Meta žymė", "LabelMetaTags": "Meta žymos", diff --git a/client/strings/nl.json b/client/strings/nl.json index 06aed9045..e05db32b9 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen", "HeaderAudiobookTools": "Audioboekbestandbeheer tools", "HeaderAudioTracks": "Audiotracks", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Back-ups", "HeaderChangePassword": "Wachtwoord wijzigen", "HeaderChapters": "Hoofdstukken", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Hard-delete bestand", "LabelHasEbook": "Heeft ebook", "LabelHasSupplementaryEbook": "Heeft supplementair ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Uur", "LabelIcon": "Icoon", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Waarschuwing", "LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Mediaspeler", "LabelMediaType": "Mediatype", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadatabron", "LabelMetaTag": "Meta-tag", "LabelMetaTags": "Meta-tags", diff --git a/client/strings/no.json b/client/strings/no.json index 7fcd1c96a..b36a00260 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise notifikasjonsinstillinger", "HeaderAudiobookTools": "Lydbok Filbehandlingsverktøy", "HeaderAudioTracks": "Lydspor", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Sikkerhetskopier", "HeaderChangePassword": "Bytt passord", "HeaderChapters": "Kapittel", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Tving sletting av fil", "LabelHasEbook": "Har ebok", "LabelHasSupplementaryEbook": "Har supplerende ebok", + "LabelHighestPriority": "Highest priority", "LabelHost": "Tjener", "LabelHour": "Time", "LabelIcon": "Ikon", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Se etter nye episoder etter denne datoen", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Mediespiller", "LabelMediaType": "Medie type", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadata Leverandør", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/pl.json b/client/strings/pl.json index dd3c1d4ad..e3729a6bb 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Ustawienia powiadomień Apprise", "HeaderAudiobookTools": "Narzędzia do zarządzania audiobookami", "HeaderAudioTracks": "Ścieżki audio", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Kopie zapasowe", "HeaderChangePassword": "Zmień hasło", "HeaderChapters": "Rozdziały", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Usuń trwale plik", "LabelHasEbook": "Has ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Godzina", "LabelIcon": "Ikona", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Informacja", "LabelLogLevelWarn": "Ostrzeżenie", "LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Odtwarzacz", "LabelMediaType": "Typ mediów", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Dostawca metadanych", "LabelMetaTag": "Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/ru.json b/client/strings/ru.json index 832ffe8b6..9caabb72a 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Настройки оповещений", "HeaderAudiobookTools": "Инструменты файлов аудиокниг", "HeaderAudioTracks": "Аудио треки", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Бэкапы", "HeaderChangePassword": "Изменить пароль", "HeaderChapters": "Главы", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Жесткое удаление файла", "LabelHasEbook": "Есть e-книга", "LabelHasSupplementaryEbook": "Есть дополнительная e-книга", + "LabelHighestPriority": "Highest priority", "LabelHost": "Хост", "LabelHour": "Часы", "LabelIcon": "Иконка", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Медиа проигрыватель", "LabelMediaType": "Тип медиа", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Провайдер", "LabelMetaTag": "Мета тег", "LabelMetaTags": "Мета теги", diff --git a/client/strings/sv.json b/client/strings/sv.json index 23d489d05..ab20e40b1 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise Meddelandeinställningar", "HeaderAudiobookTools": "Ljudbokshantering", "HeaderAudioTracks": "Ljudspår", + "HeaderAuthentication": "Authentication", "HeaderBackups": "Säkerhetskopior", "HeaderChangePassword": "Ändra lösenord", "HeaderChapters": "Kapitel", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "Hård radering av fil", "LabelHasEbook": "Har e-bok", "LabelHasSupplementaryEbook": "Har kompletterande e-bok", + "LabelHighestPriority": "Highest priority", "LabelHost": "Värd", "LabelHour": "Timme", "LabelIcon": "Ikon", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "Felsökningsnivå: Information", "LabelLogLevelWarn": "Felsökningsnivå: Varning", "LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "Mediaspelare", "LabelMediaType": "Mediatyp", - "LabelMetadataOrderOfPrecedenceDescription": "1 är lägsta prioritet, 5 är högsta prioritet", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadataleverantör", "LabelMetaTag": "Metamärke", "LabelMetaTags": "Metamärken", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 5d3de27ad..616e4f6c4 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -92,6 +92,7 @@ "HeaderAppriseNotificationSettings": "测试通知设置", "HeaderAudiobookTools": "有声读物文件管理工具", "HeaderAudioTracks": "音轨", + "HeaderAuthentication": "Authentication", "HeaderBackups": "备份", "HeaderChangePassword": "更改密码", "HeaderChapters": "章节", @@ -275,6 +276,7 @@ "LabelHardDeleteFile": "完全删除文件", "LabelHasEbook": "有电子书", "LabelHasSupplementaryEbook": "有补充电子书", + "LabelHighestPriority": "Highest priority", "LabelHost": "主机", "LabelHour": "小时", "LabelIcon": "图标", @@ -316,9 +318,10 @@ "LabelLogLevelInfo": "信息", "LabelLogLevelWarn": "警告", "LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集", + "LabelLowestPriority": "Lowest Priority", "LabelMediaPlayer": "媒体播放器", "LabelMediaType": "媒体类型", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "元数据提供者", "LabelMetaTag": "元数据标签", "LabelMetaTags": "元标签", From d9584174ffb153d812cda70af530492253ec1c1c Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 26 Nov 2023 14:33:35 -0600 Subject: [PATCH 184/208] Parse NFO trim final parsed description --- server/utils/parsers/parseNfoMetadata.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/server/utils/parsers/parseNfoMetadata.js b/server/utils/parsers/parseNfoMetadata.js index a7fbbceb4..ac41cfe60 100644 --- a/server/utils/parsers/parseNfoMetadata.js +++ b/server/utils/parsers/parseNfoMetadata.js @@ -60,7 +60,7 @@ function parseNfoMetadata(nfoText) { metadata.publishedYear = year } } - break; + break case 'position in series': metadata.sequence = value break @@ -76,19 +76,25 @@ function parseNfoMetadata(nfoText) { case 'asin': metadata.asin = value break - case 'isbn': - case 'isbn-10': - case 'isbn-13': + case 'isbn': + case 'isbn-10': + case 'isbn-13': metadata.isbn = value break - } + } } }) + + // Trim leading/trailing whitespace for description + if (metadata.description) { + metadata.description = metadata.description.trim() + } + return metadata } module.exports = { parseNfoMetadata } function extractYear(str) { const match = str.match(/\d{4}/g) - return match ? match[match.length-1] : null + return match ? match[match.length - 1] : null } \ No newline at end of file From b4c14fc78d032f896b5ffd9f0223b205c0510a43 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 26 Nov 2023 14:38:25 -0600 Subject: [PATCH 185/208] Parse NFO comma separated strings remove empty strings --- server/utils/parsers/parseNfoMetadata.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/utils/parsers/parseNfoMetadata.js b/server/utils/parsers/parseNfoMetadata.js index ac41cfe60..56e9400a7 100644 --- a/server/utils/parsers/parseNfoMetadata.js +++ b/server/utils/parsers/parseNfoMetadata.js @@ -32,20 +32,20 @@ function parseNfoMetadata(nfoText) { } break case 'author': - metadata.authors = value.split(/\s*,\s*/) + metadata.authors = value.split(/\s*,\s*/).filter(v => v) break case 'narrator': case 'read by': - metadata.narrators = value.split(/\s*,\s*/) + metadata.narrators = value.split(/\s*,\s*/).filter(v => v) break case 'series name': metadata.series = value break case 'genre': - metadata.genres = value.split(/\s*,\s*/) + metadata.genres = value.split(/\s*,\s*/).filter(v => v) break case 'tags': - metadata.tags = value.split(/\s*,\s*/) + metadata.tags = value.split(/\s*,\s*/).filter(v => v) break case 'copyright': case 'audible.com release': From 3d468339b38eef5dba72dd097f4d9eb4e9a99dd4 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 26 Nov 2023 14:41:19 -0600 Subject: [PATCH 186/208] Update parse nfo metadata test for description --- test/server/utils/parsers/parseNfoMetadata.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/server/utils/parsers/parseNfoMetadata.test.js b/test/server/utils/parsers/parseNfoMetadata.test.js index 91141335b..70e6a096f 100644 --- a/test/server/utils/parsers/parseNfoMetadata.test.js +++ b/test/server/utils/parsers/parseNfoMetadata.test.js @@ -106,7 +106,7 @@ describe('parseNfoMetadata', () => { it('parses description', () => { const nfoText = 'Book Description\n=========\nThis is a book.\n It\'s good' const result = parseNfoMetadata(nfoText) - expect(result.description).to.equal('This is a book.\n It\'s good\n') + expect(result.description).to.equal('This is a book.\n It\'s good') }) it('no value', () => { From f243ad14e09725ef5d632d956fa13033d232abb3 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 27 Nov 2023 17:10:31 -0600 Subject: [PATCH 187/208] Add help link to oidc guide --- client/pages/config/authentication.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index e2f6d6788..e645569e6 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -11,6 +11,11 @@ <div class="flex items-center"> <ui-checkbox v-model="enableOpenIDAuth" checkbox-bg="bg" /> <p class="text-lg pl-4">{{ $strings.HeaderOpenIDConnectAuthentication }}</p> + <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2"> + <a href="https://www.audiobookshelf.org/guides/oidc_authentication" target="_blank" class="inline-flex"> + <span class="material-icons text-xl w-5 text-gray-200">help_outline</span> + </a> + </ui-tooltip> </div> <transition name="slide"> From 086954fb9cf6ffaf2b51cab547615562f5341b4e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 27 Nov 2023 17:41:47 -0600 Subject: [PATCH 188/208] Version bump v2.6.0 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 6 +++--- package.json | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 1dc72e4c3..16adf9db4 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.5.0", + "version": "2.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.5.0", + "version": "2.6.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index c815d388d..a13b58158 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.5.0", + "version": "2.6.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index e1a5f266a..9df54fdde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.5.0", + "version": "2.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.5.0", + "version": "2.6.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", @@ -9487,4 +9487,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 477f62afd..061e2a7f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.5.0", + "version": "2.6.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", @@ -61,4 +61,4 @@ "nyc": "^15.1.0", "sinon": "^17.0.1" } -} +} \ No newline at end of file From ad53894ea1e57ab49c633bed177f512d976c2fdb Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Tue, 28 Nov 2023 17:29:22 +0100 Subject: [PATCH 189/208] SSO/OpenID: Provide detailed error messages --- server/Auth.js | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index dedf32f01..af2b42896 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -363,12 +363,48 @@ class Auth { req.session[sessionKey].code_verifier = req.query.code_verifier } + function handleAuthError(isMobile, errorCode, errorMessage, logMessage, logMessageDetail) { + Logger.error(logMessage) + if (logMessageDetail) { + Logger.debug(logMessageDetail.toString()) + } + + if (isMobile) { + return res.status(errorCode).send(errorMessage) + } else { + return res.redirect(`/login?error=${encodeURIComponent(errorMessage)}&autoLaunch=0`) + } + } + + function passportCallback(req, res, next) { + return (err, user, info) => { + const isMobile = req.session[sessionKey]?.mobile === true + if (err) { + return handleAuthError(isMobile, 500, 'Error in callback', `[Auth] Error in openid callback - ${err}`, err?.response?.body) + } + + if (!user) { + // Info usually contains the error message from the SSO provider + // Depending on the error, it can also have a body + return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No user in openid callback - ${info}`, info?.response?.body) + } + + req.logIn(user, (loginError) => { + if (loginError) { + return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`) + } + next() + }) + } + } + + // While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request // We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided if (req.session[sessionKey].mobile) { - return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' })(req, res, next) + return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' }, passportCallback(req, res, next))(req, res, next) } else { - return passport.authenticate('openid-client', { failureRedirect: '/login?error=Unauthorized&autoLaunch=0' })(req, res, next) + return passport.authenticate('openid-client', passportCallback(req, res, next))(req, res, next) } }, // on a successfull login: read the cookies and react like the client requested (callback or json) From 618028503bcd4033ef664a0747abd4a80a594635 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Tue, 28 Nov 2023 20:07:49 +0100 Subject: [PATCH 190/208] SSO/OpenID: Also Log token header --- server/Auth.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index af2b42896..74ccf2408 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -363,10 +363,13 @@ class Auth { req.session[sessionKey].code_verifier = req.query.code_verifier } - function handleAuthError(isMobile, errorCode, errorMessage, logMessage, logMessageDetail) { + function handleAuthError(isMobile, errorCode, errorMessage, logMessage, response) { Logger.error(logMessage) - if (logMessageDetail) { - Logger.debug(logMessageDetail.toString()) + if (response) { + // Depending on the error, it can also have a body + // We also log the request header the passport plugin sents for the URL + const header = response.req?._header.replace(/Authorization: [^\r\n]*/i, 'Authorization: REDACTED') + Logger.debug(header + '\n' + response.body?.toString()) } if (isMobile) { @@ -380,13 +383,12 @@ class Auth { return (err, user, info) => { const isMobile = req.session[sessionKey]?.mobile === true if (err) { - return handleAuthError(isMobile, 500, 'Error in callback', `[Auth] Error in openid callback - ${err}`, err?.response?.body) + return handleAuthError(isMobile, 500, 'Error in callback', `[Auth] Error in openid callback - ${err}`, err?.response) } if (!user) { // Info usually contains the error message from the SSO provider - // Depending on the error, it can also have a body - return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No user in openid callback - ${info}`, info?.response?.body) + return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No user in openid callback - ${info}`, info?.response) } req.logIn(user, (loginError) => { From e5579b2c3346c816856e7a9f47740661c6c66699 Mon Sep 17 00:00:00 2001 From: Kieran Eglin <kieran.eglin@gmail.com> Date: Tue, 28 Nov 2023 11:45:44 -0800 Subject: [PATCH 191/208] Improved UI; Added tooltips; Fixed unrelated layout issues --- client/components/cards/ItemUploadCard.vue | 27 ++++++++++++---------- client/pages/upload/index.vue | 16 +++++++++---- client/strings/en-us.json | 2 ++ 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index 68b77d56a..a393a170a 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -8,12 +8,6 @@ <span class="text-base text-white text-opacity-80 font-mono material-icons">close</span> </div> - <div v-if="!isPodcast" - class="w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" - @click="fetchMetadata"> - <span class="text-base text-white text-opacity-80 font-mono material-icons">refresh</span> - </div> - <template v-if="!uploadSuccess && !uploadFailed"> <widgets-alert v-if="error" type="error"> <p class="text-base">{{ error }}</p> @@ -21,24 +15,33 @@ <div class="flex my-2 -mx-2"> <div class="w-1/2 px-2"> - <ui-text-input-with-label v-model="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" /> + <ui-text-input-with-label v-model.trim="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" /> </div> <div class="w-1/2 px-2"> - <ui-text-input-with-label v-if="!isPodcast" v-model="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" /> + <div v-if="!isPodcast" class="flex items-end"> + <ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" /> + <ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp"> + <div + class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" + @click="fetchMetadata"> + <span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span> + </div> + </ui-tooltip> + </div> <div v-else class="w-full"> <p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p> - <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" /> + <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" /> </div> </div> </div> <div v-if="!isPodcast" class="flex my-2 -mx-2"> <div class="w-1/2 px-2"> - <ui-text-input-with-label v-model="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" /> + <ui-text-input-with-label v-model.trim="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" inputClass="h-10" /> </div> <div class="w-1/2 px-2"> <div class="w-full"> - <p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p> - <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" /> + <label class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></label> + <ui-text-input :value="directory" disabled class="w-full font-mono text-xs h-10" /> </div> </div> </div> diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue index 8dd139907..e29239a0d 100644 --- a/client/pages/upload/index.vue +++ b/client/pages/upload/index.vue @@ -15,10 +15,16 @@ </div> <div v-if="!selectedLibraryIsPodcast" class="flex items-center py-2"> - <ui-toggle-switch v-model="fetchMetadata.enabled" /> - <p class="pl-4 text-base">{{ $strings.LabelAutoFetchMetadata }}</p> + <label class="flex cursor-pointer"> + <ui-toggle-switch v-model="fetchMetadata.enabled" /> + <span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span> + </label> + <ui-tooltip :text="$strings.LabelAutoFetchMetadataHelp"> + <span class="pl-1 material-icons icon-text text-sm cursor-pointer">info_outlined</span> + </ui-tooltip> + <div class="flex-grow ml-4"> - <ui-dropdown v-model="fetchMetadata.provider" :items="providers" :label="$strings.LabelProvider" :disabled="!canFetchMetadata" /> + <ui-dropdown v-model="fetchMetadata.provider" :items="providers" :label="$strings.LabelProvider" /> </div> </div> @@ -110,7 +116,7 @@ export default { uploadFinished: false, fetchMetadata: { enabled: false, - provider: 'google' + provider: null } } }, @@ -195,7 +201,7 @@ export default { } }, setMetadataProvider() { - this.fetchMetadata.provider = this.$store.getters['libraries/getLibraryProvider'](this.selectedLibraryId) + this.fetchMetadata.provider ||= this.$store.getters['libraries/getLibraryProvider'](this.selectedLibraryId) }, removeItem(item) { this.items = this.items.filter((b) => b.index !== item.index) diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 78049f247..5b73de363 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -197,6 +197,7 @@ "LabelAuthors": "Authors", "LabelAutoDownloadEpisodes": "Auto Download Episodes", "LabelAutoFetchMetadata": "Auto Fetch Metadata", + "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.", "LabelAutoLaunch": "Auto Launch", "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", "LabelAutoRegister": "Auto Register", @@ -517,6 +518,7 @@ "LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located", "LabelUploaderDragAndDrop": "Drag & drop files or folders", "LabelUploaderDropFiles": "Drop files", + "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", "LabelUseChapterTrack": "Use chapter track", "LabelUseFullTrack": "Use full track", "LabelUser": "User", From d9c9289d655f6050e1db97c62a62944eaf5591e9 Mon Sep 17 00:00:00 2001 From: Kieran Eglin <kieran.eglin@gmail.com> Date: Tue, 28 Nov 2023 12:11:14 -0800 Subject: [PATCH 192/208] Added error handling; Made querystring helper --- client/components/cards/ItemUploadCard.vue | 27 +++++++++++++++------- client/mixins/apiRequestHelpers.js | 12 ++++++++++ client/strings/en-us.json | 3 +++ 3 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 client/mixins/apiRequestHelpers.js diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index a393a170a..4a40f2b75 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -65,8 +65,10 @@ <script> import Path from 'path' +import apiRequestHelpers from '@/mixins/apiRequestHelpers' export default { + mixins: [apiRequestHelpers], props: { item: { type: Object, @@ -132,27 +134,36 @@ export default { } this.isFetchingMetadata = true + this.error = '' try { - const searchQueryString = `title=${this.itemData.title}&author=${this.itemData.author}&provider=${this.provider}` + const searchQueryString = this.buildQuerystring({ + title: this.itemData.title, + author: this.itemData.author, + provider: this.provider + }) const [bestCandidate, ..._rest] = await this.$axios.$get(`/api/search/books?${searchQueryString}`) - this.itemData = { - ...this.itemData, - title: bestCandidate?.title, - author: bestCandidate?.author, - series: (bestCandidate?.series || [])[0]?.series + if (bestCandidate) { + this.itemData = { + ...this.itemData, + title: bestCandidate?.title, + author: bestCandidate?.author, + series: (bestCandidate?.series || [])[0]?.series + } + } else { + this.error = this.$strings.ErrorUploadFetchMetadataNoResults } } catch (e) { console.error('Failed', e) - // TODO: do something with the error? + this.error = this.$strings.ErrorUploadFetchMetadataAPI } finally { this.isFetchingMetadata = false } }, getData() { if (!this.itemData.title) { - this.error = 'Must have a title' + this.error = this.$strings.ErrorUploadLacksTitle return null } this.error = '' diff --git a/client/mixins/apiRequestHelpers.js b/client/mixins/apiRequestHelpers.js new file mode 100644 index 000000000..41a28b4d3 --- /dev/null +++ b/client/mixins/apiRequestHelpers.js @@ -0,0 +1,12 @@ +export default { + methods: { + buildQuerystring(obj, opts = { includePrefix: false }) { + let querystring = Object + .entries(obj) + .map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) + .join('&') + + return (opts.includePrefix ? '?' : '').concat(querystring) + } + } +} diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 5b73de363..857627e95 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -87,6 +87,9 @@ "ButtonUserEdit": "Edit user {0}", "ButtonViewAll": "View All", "ButtonYes": "Yes", + "ErrorUploadFetchMetadataAPI": "Error fetching metadata", + "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", + "ErrorUploadLacksTitle": "Must have a title", "HeaderAccount": "Account", "HeaderAdvanced": "Advanced", "HeaderAppriseNotificationSettings": "Apprise Notification Settings", From 36599a2984c539c3d22d7f058c3101a328427572 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Tue, 28 Nov 2023 21:16:39 +0100 Subject: [PATCH 193/208] SSO/OpenID: Rename probably misleading message --- server/Auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Auth.js b/server/Auth.js index 74ccf2408..96a2bc9eb 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -388,7 +388,7 @@ class Auth { if (!user) { // Info usually contains the error message from the SSO provider - return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No user in openid callback - ${info}`, info?.response) + return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No data in openid callback - ${info}`, info?.response) } req.logIn(user, (loginError) => { From a719065b8d8d24c0d5b7c1ce10ea3e112b6e1c39 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 28 Nov 2023 16:37:19 -0600 Subject: [PATCH 194/208] Auto formatting --- server/Auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 96a2bc9eb..577921779 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -379,7 +379,7 @@ class Auth { } } - function passportCallback(req, res, next) { + function passportCallback(req, res, next) { return (err, user, info) => { const isMobile = req.session[sessionKey]?.mobile === true if (err) { @@ -390,7 +390,7 @@ class Auth { // Info usually contains the error message from the SSO provider return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No data in openid callback - ${info}`, info?.response) } - + req.logIn(user, (loginError) => { if (loginError) { return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`) From 166477ae27e9b645ce34d62eff6d69c9d97bbe49 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 28 Nov 2023 16:39:52 -0600 Subject: [PATCH 195/208] Fix:Narrators page 404 on reload #2359 --- server/Server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/Server.js b/server/Server.js index 4883fb715..5e8cab760 100644 --- a/server/Server.js +++ b/server/Server.js @@ -231,6 +231,7 @@ class Server { '/library/:library/search', '/library/:library/bookshelf/:id?', '/library/:library/authors', + '/library/:library/narrators', '/library/:library/series/:id?', '/library/:library/podcast/search', '/library/:library/podcast/latest', From 88078ff813ac1763eb1fa7b94d42e381e1cb67a6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 1 Dec 2023 16:44:04 -0600 Subject: [PATCH 196/208] Fix undefined series string when match has no series, minor ui updates --- client/pages/upload/index.vue | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue index e29239a0d..547f5b05a 100644 --- a/client/pages/upload/index.vue +++ b/client/pages/upload/index.vue @@ -14,12 +14,12 @@ </div> </div> - <div v-if="!selectedLibraryIsPodcast" class="flex items-center py-2"> - <label class="flex cursor-pointer"> - <ui-toggle-switch v-model="fetchMetadata.enabled" /> + <div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6"> + <label class="flex cursor-pointer pt-4"> + <ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" /> <span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span> </label> - <ui-tooltip :text="$strings.LabelAutoFetchMetadataHelp"> + <ui-tooltip :text="$strings.LabelAutoFetchMetadataHelp" class="inline-flex pt-4"> <span class="pl-1 material-icons icon-text text-sm cursor-pointer">info_outlined</span> </ui-tooltip> @@ -75,16 +75,7 @@ </widgets-alert> <!-- Item Upload cards --> - <cards-item-upload-card - v-for="item in items" - :key="item.index" - :ref="`itemCard-${item.index}`" - :media-type="selectedLibraryMediaType" - :item="item" - :provider="fetchMetadata.provider" - :processing="processing" - @remove="removeItem(item)" - /> + <cards-item-upload-card v-for="item in items" :key="item.index" :ref="`itemCard-${item.index}`" :media-type="selectedLibraryMediaType" :item="item" :provider="fetchMetadata.provider" :processing="processing" @remove="removeItem(item)" /> <!-- Upload/Reset btns --> <div v-show="items.length" class="flex justify-end pb-8 pt-4"> @@ -307,8 +298,8 @@ export default { var form = new FormData() form.set('title', item.title) if (!this.selectedLibraryIsPodcast) { - form.set('author', item.author) - form.set('series', item.series) + form.set('author', item.author || '') + form.set('series', item.series || '') } form.set('library', this.selectedLibraryId) form.set('folder', this.selectedFolderId) From f59516cc6eaaab792c95b9c991820e3cc057886f Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 1 Dec 2023 17:10:33 -0600 Subject: [PATCH 197/208] Fix:Hide change password form when password auth is disabled #2367 --- client/pages/account.vue | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/pages/account.vue b/client/pages/account.vue index c582c2640..4bb68727b 100644 --- a/client/pages/account.vue +++ b/client/pages/account.vue @@ -19,8 +19,8 @@ <div class="w-full h-px bg-white/10 my-4" /> - <p v-if="!isGuest" class="mb-4 text-lg">{{ $strings.HeaderChangePassword }}</p> - <form v-if="!isGuest" @submit.prevent="submitChangePassword"> + <p v-if="showChangePasswordForm" class="mb-4 text-lg">{{ $strings.HeaderChangePassword }}</p> + <form v-if="showChangePasswordForm" @submit.prevent="submitChangePassword"> <ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" :label="$strings.LabelPassword" class="my-2" /> <ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" :label="$strings.LabelNewPassword" class="my-2" /> <ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" :label="$strings.LabelConfirmPassword" class="my-2" /> @@ -68,6 +68,13 @@ export default { }, isGuest() { return this.usertype === 'guest' + }, + isPasswordAuthEnabled() { + const activeAuthMethods = this.$store.getters['getServerSetting']('authActiveAuthMethods') || [] + return activeAuthMethods.includes('local') + }, + showChangePasswordForm() { + return !this.isGuest && this.isPasswordAuthEnabled } }, methods: { From 9350c5513ebeee88d686f77530f548b6ee92efb4 Mon Sep 17 00:00:00 2001 From: Kieran Eglin <kieran.eglin@gmail.com> Date: Fri, 1 Dec 2023 15:19:50 -0800 Subject: [PATCH 198/208] Removed unneeded mixin --- client/components/cards/ItemUploadCard.vue | 10 ++++------ client/mixins/apiRequestHelpers.js | 12 ------------ 2 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 client/mixins/apiRequestHelpers.js diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index 4a40f2b75..5806f1053 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -65,10 +65,8 @@ <script> import Path from 'path' -import apiRequestHelpers from '@/mixins/apiRequestHelpers' export default { - mixins: [apiRequestHelpers], props: { item: { type: Object, @@ -137,7 +135,7 @@ export default { this.error = '' try { - const searchQueryString = this.buildQuerystring({ + const searchQueryString = new URLSearchParams({ title: this.itemData.title, author: this.itemData.author, provider: this.provider @@ -147,9 +145,9 @@ export default { if (bestCandidate) { this.itemData = { ...this.itemData, - title: bestCandidate?.title, - author: bestCandidate?.author, - series: (bestCandidate?.series || [])[0]?.series + title: bestCandidate.title, + author: bestCandidate.author, + series: (bestCandidate.series || [])[0]?.series } } else { this.error = this.$strings.ErrorUploadFetchMetadataNoResults diff --git a/client/mixins/apiRequestHelpers.js b/client/mixins/apiRequestHelpers.js deleted file mode 100644 index 41a28b4d3..000000000 --- a/client/mixins/apiRequestHelpers.js +++ /dev/null @@ -1,12 +0,0 @@ -export default { - methods: { - buildQuerystring(obj, opts = { includePrefix: false }) { - let querystring = Object - .entries(obj) - .map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) - .join('&') - - return (opts.includePrefix ? '?' : '').concat(querystring) - } - } -} From 57a5005197e3864025c0638470766ab93d81c833 Mon Sep 17 00:00:00 2001 From: Kieran Eglin <kieran.eglin@gmail.com> Date: Fri, 1 Dec 2023 21:42:54 -0800 Subject: [PATCH 199/208] Addressed feedback changes --- client/components/cards/ItemUploadCard.vue | 11 ++-- client/plugins/init.client.js | 1 + server/controllers/MiscController.js | 58 ++++++++-------------- server/utils/fileUtils.js | 1 + 4 files changed, 27 insertions(+), 44 deletions(-) diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index 5806f1053..075c0fd4e 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -98,13 +98,10 @@ export default { if (!this.itemData.title) return '' if (this.isPodcast) return this.itemData.title - if (this.itemData.series && this.itemData.author) { - return Path.join(this.itemData.author, this.itemData.series, this.itemData.title) - } else if (this.itemData.author) { - return Path.join(this.itemData.author, this.itemData.title) - } else { - return this.itemData.title - } + const outputPathParts = [this.itemData.author, this.itemData.series, this.itemData.title] + const cleanedOutputPathParts = outputPathParts.filter(Boolean).map(part => this.$sanitizeFilename(part)) + + return Path.join(...cleanedOutputPathParts) }, isNonInteractable() { return this.isUploading || this.isFetchingMetadata diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js index 711c526ad..a16e6fa10 100644 --- a/client/plugins/init.client.js +++ b/client/plugins/init.client.js @@ -77,6 +77,7 @@ Vue.prototype.$sanitizeFilename = (filename, colonReplacement = ' - ') => { .replace(lineBreaks, replacement) .replace(windowsReservedRe, replacement) .replace(windowsTrailingRe, replacement) + .replace(/\s+/g, ' ') // Replace consecutive spaces with a single space // Check if basename is too many bytes const ext = Path.extname(sanitized) // separate out file extension diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 267db5c8e..26a9d77bc 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -8,6 +8,7 @@ const Database = require('../Database') const libraryItemFilters = require('../utils/queries/libraryItemFilters') const patternValidation = require('../libs/nodeCron/pattern-validation') const { isObject, getTitleIgnorePrefix } = require('../utils/index') +const { sanitizeFilename } = require('../utils/fileUtils') const TaskManager = require('../managers/TaskManager') @@ -32,12 +33,9 @@ class MiscController { Logger.error('Invalid request, no files') return res.sendStatus(400) } + const files = Object.values(req.files) - const title = req.body.title - const author = req.body.author - const series = req.body.series - const libraryId = req.body.library - const folderId = req.body.folder + const { title, author, series, folder: folderId, library: libraryId } = req.body const library = await Database.libraryModel.getOldById(libraryId) if (!library) { @@ -52,43 +50,29 @@ class MiscController { return res.status(500).send(`Invalid post data`) } - // For setting permissions recursively - let outputDirectory = '' - let firstDirPath = '' - - if (library.isPodcast) { // Podcasts only in 1 folder - outputDirectory = Path.join(folder.fullPath, title) - firstDirPath = outputDirectory - } else { - firstDirPath = Path.join(folder.fullPath, author) - if (series && author) { - outputDirectory = Path.join(folder.fullPath, author, series, title) - } else if (author) { - outputDirectory = Path.join(folder.fullPath, author, title) - } else { - outputDirectory = Path.join(folder.fullPath, title) - } - } - - if (await fs.pathExists(outputDirectory)) { - Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`) - return res.status(500).send(`Directory "${outputDirectory}" already exists`) - } + // Podcasts should only be one folder deep + const outputDirectoryParts = library.isPodcast ? [title] : [author, series, title] + // `.filter(Boolean)` to strip out all the potentially missing details (eg: `author`) + // before sanitizing all the directory parts to remove illegal chars and finally prepending + // the base folder path + const cleanedOutputDirectoryParts = outputDirectoryParts.filter(Boolean).map(part => sanitizeFilename(part)) + const outputDirectory = Path.join(...[folder.fullPath, ...cleanedOutputDirectoryParts]) await fs.ensureDir(outputDirectory) Logger.info(`Uploading ${files.length} files to`, outputDirectory) - for (let i = 0; i < files.length; i++) { - var file = files[i] + for (const file of files) { + const path = Path.join(outputDirectory, sanitizeFilename(file.name)) - var path = Path.join(outputDirectory, file.name) - await file.mv(path).then(() => { - return true - }).catch((error) => { - Logger.error('Failed to move file', path, error) - return false - }) + await file.mv(path) + .then(() => { + return true + }) + .catch((error) => { + Logger.error('Failed to move file', path, error) + return false + }) } res.sendStatus(200) @@ -691,4 +675,4 @@ class MiscController { }) } } -module.exports = new MiscController() \ No newline at end of file +module.exports = new MiscController() diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 26578f573..ebad97dbc 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -308,6 +308,7 @@ module.exports.sanitizeFilename = (filename, colonReplacement = ' - ') => { .replace(lineBreaks, replacement) .replace(windowsReservedRe, replacement) .replace(windowsTrailingRe, replacement) + .replace(/\s+/g, ' ') // Replace consecutive spaces with a single space // Check if basename is too many bytes const ext = Path.extname(sanitized) // separate out file extension From 84160b2f07164605295d6cb6f7f7925cbdf538e4 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 2 Dec 2023 16:17:52 -0600 Subject: [PATCH 200/208] Fix:Server crash when user without a password attempts to login with a password #2378 --- server/Auth.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 577921779..267bbb459 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -542,13 +542,13 @@ class Auth { // Load the user given it's username const user = await Database.userModel.getUserByUsername(username.toLowerCase()) - if (!user || !user.isActive) { + if (!user?.isActive) { done(null, null) return } // Check passwordless root user - if (user.type === 'root' && (!user.pash || user.pash === '')) { + if (user.type === 'root' && !user.pash) { if (password) { // deny login done(null, null) @@ -557,6 +557,10 @@ class Auth { // approve login done(null, user) return + } else if (!user.pash) { + Logger.error(`[Auth] User "${user.username}"/"${user.type}" attempted to login without a password set`) + done(null, null) + return } // Check password match From 80fd2a1a1831b415546194fc2e7809a002f85030 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Mon, 4 Dec 2023 22:36:34 +0100 Subject: [PATCH 201/208] SSO/OpenID: Use a mobile-redirect route (Fixes #2379 and #2381) - Implement /auth/openid/mobile-redirect this will redirect to an app-link like audiobookshelf://oauth - An app must provide an `redirect_uri` parameter with the app-link in the authorization request to /auth/openid - The user will have to whitelist possible URLs, or explicitly allow all - Also modified MultiSelect to allow to hide the menu/popup --- client/components/ui/MultiSelect.vue | 8 ++- client/pages/config/authentication.vue | 22 +++++++++ client/strings/de.json | 2 + client/strings/en-us.json | 2 + server/Auth.js | 59 ++++++++++++++++++++++- server/controllers/MiscController.js | 17 +++++++ server/objects/settings/ServerSettings.js | 9 +++- 7 files changed, 114 insertions(+), 5 deletions(-) diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue index 4fa8e394f..2009b28dd 100644 --- a/client/components/ui/MultiSelect.vue +++ b/client/components/ui/MultiSelect.vue @@ -50,7 +50,11 @@ export default { label: String, disabled: Boolean, readonly: Boolean, - showEdit: Boolean + showEdit: Boolean, + menuDisabled: { + type: Boolean, + default: false + }, }, data() { return { @@ -77,7 +81,7 @@ export default { } }, showMenu() { - return this.isFocused + return this.isFocused && !this.menuDisabled }, wrapperClass() { var classes = [] diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index e645569e6..ffb1feb7a 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -46,6 +46,9 @@ <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" /> + <ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" /> + <p class="pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" /> + <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" /> <div class="flex items-center pt-1 mb-2"> @@ -187,6 +190,25 @@ export default { this.$toast.error('Client Secret required') isValid = false } + + function isValidRedirectURI(uri) { + // Check for somestring://someother/string + const pattern = new RegExp('^\\w+://[\\w\\.-]+$', 'i') + return pattern.test(uri) + } + + const uris = this.newAuthSettings.authOpenIDMobileRedirectURIs + if (uris.includes('*') && uris.length > 1) { + this.$toast.error('Mobile Redirect URIs: Asterisk (*) must be the only entry if used') + isValid = false + } else { + uris.forEach(uri => { + if (uri !== '*' && !isValidRedirectURI(uri)) { + this.$toast.error(`Mobile Redirect URIs: Invalid URI ${uri}`) + isValid = false + } + }) + } return isValid }, async saveSettings() { diff --git a/client/strings/de.json b/client/strings/de.json index 78e648040..eb3d59f44 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -337,6 +337,8 @@ "LabelMinute": "Minute", "LabelMissing": "Fehlend", "LabelMissingParts": "Fehlende Teile", + "LabelMobileRedirectURIs": "Erlaubte Weiterleitungs-URIs für die mobile App", + "LabelMobileRedirectURIsDescription": "Dies ist eine Whitelist gültiger Umleitungs-URIs für mobile Apps. Der Standardwert ist <code>audiobookshelf://oauth</code>, den Sie entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen können. Die Verwendung eines Sternchens (<code>*</code>) als alleiniger Eintrag erlaubt jede URI.", "LabelMore": "Mehr", "LabelMoreInfo": "Mehr Info", "LabelName": "Name", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 857627e95..02f9df053 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -343,6 +343,8 @@ "LabelMinute": "Minute", "LabelMissing": "Missing", "LabelMissingParts": "Missing Parts", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "More", "LabelMoreInfo": "More Info", "LabelName": "Name", diff --git a/server/Auth.js b/server/Auth.js index 267bbb459..c20d532ab 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -8,6 +8,7 @@ const ExtractJwt = require('passport-jwt').ExtractJwt const OpenIDClient = require('openid-client') const Database = require('./Database') const Logger = require('./Logger') +const e = require('express') /** * @class Class for handling all the authentication related functionality. @@ -15,6 +16,8 @@ const Logger = require('./Logger') class Auth { constructor() { + // Map of openId sessions indexed by oauth2 state-variable + this.openIdAuthSession = new Map() } /** @@ -283,7 +286,26 @@ class Auth { // for API or mobile clients const oidcStrategy = passport._strategy('openid-client') const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http' - oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString() + + let redirect_uri = null + + // The client wishes a different redirect_uri + // We will allow if it is in the whitelist, by saving it into this.openIdAuthSession and setting the redirect uri to /auth/openid/mobile-redirect + // where we will handle the redirect to it + if (req.query.redirect_uri) { + // Check if the redirect_uri is in the whitelist + if (Database.serverSettings.authOpenIDMobileRedirectURIs.includes(req.query.redirect_uri) || + (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) { + oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/mobile-redirect`).toString() + redirect_uri = req.query.redirect_uri + } else { + Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri} - not in whitelist`) + return res.status(400).send('Invalid redirect_uri') + } + } else { + oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString() + } + Logger.debug(`[Auth] Set oidc redirect_uri=${oidcStrategy._params.redirect_uri}`) const client = oidcStrategy._client const sessionKey = oidcStrategy._key @@ -327,6 +349,10 @@ class Auth { mobile: req.query.isRest?.toLowerCase() === 'true' // Used in the abs callback later } + // We cannot save redirect_uri in the session, because it the mobile client uses browser instead of the API + // for the request to mobile-redirect and as such the session is not shared + this.openIdAuthSession.set(params.state, { redirect_uri: redirect_uri }) + // Now get the URL to direct to const authorizationUrl = client.authorizationUrl({ ...params, @@ -347,6 +373,37 @@ class Auth { } }) + // This will be the oauth2 callback route for mobile clients + // It will redirect to an app-link like audiobookshelf://oauth + router.get('/auth/openid/mobile-redirect', (req, res) => { + try { + // Extract the state parameter from the request + const { state, code } = req.query + + // Check if the state provided is in our list + if (!state || !this.openIdAuthSession.has(state)) { + Logger.error('[Auth] /auth/openid/mobile-redirect route: State parameter mismatch') + return res.status(400).send('State parameter mismatch') + } + + let redirect_uri = this.openIdAuthSession.get(state).redirect_uri + + if (!redirect_uri) { + Logger.error('[Auth] No redirect URI') + return res.status(400).send('No redirect URI') + } + + this.openIdAuthSession.delete(state) + + const redirectUri = `${redirect_uri}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}` + // Redirect to the overwrite URI saved in the map + res.redirect(redirectUri) + } catch (error) { + Logger.error(`[Auth] Error in /auth/openid/mobile-redirect route: ${error}`) + res.status(500).send('Internal Server Error') + } + }) + // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', (req, res, next) => { const oidcStrategy = passport._strategy('openid-client') diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 26a9d77bc..e209fac97 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -629,6 +629,23 @@ class MiscController { } else { Logger.warn(`[MiscController] Invalid value for authActiveAuthMethods`) } + } else if (key === 'authOpenIDMobileRedirectURIs') { + function isValidRedirectURI(uri) { + const pattern = new RegExp('^\\w+://[\\w.-]+$', 'i'); + return pattern.test(uri); + } + + const uris = settingsUpdate[key] + if (!Array.isArray(uris) || + (uris.includes('*') && uris.length > 1) || + uris.some(uri => uri !== '*' && !isValidRedirectURI(uri))) { + Logger.warn(`[MiscController] Invalid value for authOpenIDMobileRedirectURIs`) + continue + } + + // Update the URIs + Database.serverSettings[key] = uris + hasUpdates = true } else { const updatedValueType = typeof settingsUpdate[key] if (['authOpenIDAutoLaunch', 'authOpenIDAutoRegister'].includes(key)) { diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index bf3db5577..6e9d84566 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -71,6 +71,7 @@ class ServerSettings { this.authOpenIDAutoLaunch = false this.authOpenIDAutoRegister = false this.authOpenIDMatchExistingBy = null + this.authOpenIDMobileRedirectURIs = ['audiobookshelf://oauth'] if (settings) { this.construct(settings) @@ -126,6 +127,7 @@ class ServerSettings { this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null + this.authOpenIDMobileRedirectURIs = settings.authOpenIDMobileRedirectURIs || ['audiobookshelf://oauth'] if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] @@ -211,7 +213,8 @@ class ServerSettings { authOpenIDButtonText: this.authOpenIDButtonText, authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, authOpenIDAutoRegister: this.authOpenIDAutoRegister, - authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy + authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy, + authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client } } @@ -220,6 +223,7 @@ class ServerSettings { delete json.tokenSecret delete json.authOpenIDClientID delete json.authOpenIDClientSecret + delete json.authOpenIDMobileRedirectURIs return json } @@ -254,7 +258,8 @@ class ServerSettings { authOpenIDButtonText: this.authOpenIDButtonText, authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, authOpenIDAutoRegister: this.authOpenIDAutoRegister, - authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy + authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy, + authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client } } From e6ab28365fa740b72295668b924ee5b1d6640f09 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Tue, 5 Dec 2023 00:18:58 +0100 Subject: [PATCH 202/208] SSO/OpenID: Remove modifying redirect_uri in the callback The redirect URI will be now correctly set to either /callback or /mobile-redirect in the /auth/openid route --- server/Auth.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index c20d532ab..b5bc7d40a 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -359,7 +359,7 @@ class Auth { scope: 'openid profile email', response_type: 'code', code_challenge, - code_challenge_method, + code_challenge_method }) // params (isRest, callback) to a cookie that will be send to the client @@ -460,11 +460,8 @@ class Auth { // While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request // We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided - if (req.session[sessionKey].mobile) { - return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' }, passportCallback(req, res, next))(req, res, next) - } else { - return passport.authenticate('openid-client', passportCallback(req, res, next))(req, res, next) - } + // This is already done in the strategy in the route to /auth/openid using oidcStrategy._params.redirect_uri + return passport.authenticate('openid-client', passportCallback(req, res, next))(req, res, next) }, // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this)) From cf00650c6d3bd74ddb9fae92138c00f808511150 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Tue, 5 Dec 2023 09:43:06 +0100 Subject: [PATCH 203/208] SSO/OpenID: Also fix possible race condition - We need to define redirect_uri in the callback again, because the global params of passport can change between calls to the first route (ie. if multiple users log in at same time) - Removed is_rest parameter as requirement for mobile flow (to maximise compatibility with possible oauth libraries) - Also renamed some variables for clarity --- server/Auth.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index b5bc7d40a..0a282c9c9 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -190,9 +190,10 @@ class Auth { * @param {import('express').Response} res */ paramsToCookies(req, res) { - if (req.query.isRest?.toLowerCase() == 'true') { + // Set if isRest flag is set or if mobile oauth flow is used + if (req.query.isRest?.toLowerCase() == 'true' || req.query.redirect_uri) { // store the isRest flag to the is_rest cookie - res.cookie('is_rest', req.query.isRest.toLowerCase(), { + res.cookie('is_rest', 'true', { maxAge: 120000, // 2 min httpOnly: true }) @@ -287,7 +288,7 @@ class Auth { const oidcStrategy = passport._strategy('openid-client') const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http' - let redirect_uri = null + let mobile_redirect_uri = null // The client wishes a different redirect_uri // We will allow if it is in the whitelist, by saving it into this.openIdAuthSession and setting the redirect uri to /auth/openid/mobile-redirect @@ -297,7 +298,7 @@ class Auth { if (Database.serverSettings.authOpenIDMobileRedirectURIs.includes(req.query.redirect_uri) || (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) { oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/mobile-redirect`).toString() - redirect_uri = req.query.redirect_uri + mobile_redirect_uri = req.query.redirect_uri } else { Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri} - not in whitelist`) return res.status(400).send('Invalid redirect_uri') @@ -306,7 +307,7 @@ class Auth { oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString() } - Logger.debug(`[Auth] Set oidc redirect_uri=${oidcStrategy._params.redirect_uri}`) + Logger.debug(`[Auth] Oidc redirect_uri=${oidcStrategy._params.redirect_uri}`) const client = oidcStrategy._client const sessionKey = oidcStrategy._key @@ -346,12 +347,13 @@ class Auth { req.session[sessionKey] = { ...req.session[sessionKey], ...pick(params, 'nonce', 'state', 'max_age', 'response_type'), - mobile: req.query.isRest?.toLowerCase() === 'true' // Used in the abs callback later + mobile: req.query.redirect_uri, // Used in the abs callback later, set mobile if redirect_uri is filled out + sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback } // We cannot save redirect_uri in the session, because it the mobile client uses browser instead of the API // for the request to mobile-redirect and as such the session is not shared - this.openIdAuthSession.set(params.state, { redirect_uri: redirect_uri }) + this.openIdAuthSession.set(params.state, { mobile_redirect_uri: mobile_redirect_uri }) // Now get the URL to direct to const authorizationUrl = client.authorizationUrl({ @@ -386,16 +388,16 @@ class Auth { return res.status(400).send('State parameter mismatch') } - let redirect_uri = this.openIdAuthSession.get(state).redirect_uri + let mobile_redirect_uri = this.openIdAuthSession.get(state).mobile_redirect_uri - if (!redirect_uri) { + if (!mobile_redirect_uri) { Logger.error('[Auth] No redirect URI') return res.status(400).send('No redirect URI') } this.openIdAuthSession.delete(state) - const redirectUri = `${redirect_uri}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}` + const redirectUri = `${mobile_redirect_uri}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}` // Redirect to the overwrite URI saved in the map res.redirect(redirectUri) } catch (error) { @@ -460,8 +462,8 @@ class Auth { // While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request // We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided - // This is already done in the strategy in the route to /auth/openid using oidcStrategy._params.redirect_uri - return passport.authenticate('openid-client', passportCallback(req, res, next))(req, res, next) + // We set it here again because the passport param can change between requests + return passport.authenticate('openid-client', { redirect_uri: req.session[sessionKey].sso_redirect_uri }, passportCallback(req, res, next))(req, res, next) }, // on a successfull login: read the cookies and react like the client requested (callback or json) this.handleLoginSuccessBasedOnCookie.bind(this)) From 34156af40330737cb188977a60960c6f78d53ef9 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 5 Dec 2023 17:58:54 -0600 Subject: [PATCH 204/208] Fix:Updating media progress not clearing cache #2392 --- server/managers/ApiCacheManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/managers/ApiCacheManager.js b/server/managers/ApiCacheManager.js index c6579ab34..1af069f36 100644 --- a/server/managers/ApiCacheManager.js +++ b/server/managers/ApiCacheManager.js @@ -13,7 +13,7 @@ class ApiCacheManager { } init(database = Database) { - let hooks = ['afterCreate', 'afterUpdate', 'afterDestroy', 'afterBulkCreate', 'afterBulkUpdate', 'afterBulkDestroy'] + let hooks = ['afterCreate', 'afterUpdate', 'afterDestroy', 'afterBulkCreate', 'afterBulkUpdate', 'afterBulkDestroy', 'afterUpsert'] hooks.forEach(hook => database.sequelize.addHook(hook, (model) => this.clear(model, hook))) } From b5e255a3848637c636f92dd1110010fbdcb69e6c Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 6 Dec 2023 17:31:36 -0600 Subject: [PATCH 205/208] Update:Clean series sequence response from audible provider #2380 - Removes Book prefix - Splits on spaces and takes first, removes trailing comma --- server/providers/Audible.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/server/providers/Audible.js b/server/providers/Audible.js index 31719eef1..e46ed3232 100644 --- a/server/providers/Audible.js +++ b/server/providers/Audible.js @@ -18,6 +18,27 @@ class Audible { } } + /** + * Audible will sometimes send sequences with "Book 1" or "2, Dramatized Adaptation" + * @see https://github.com/advplyr/audiobookshelf/issues/2380 + * @see https://github.com/advplyr/audiobookshelf/issues/1339 + * + * @param {string} seriesName + * @param {string} sequence + * @returns {string} + */ + cleanSeriesSequence(seriesName, sequence) { + if (!sequence) return '' + let updatedSequence = sequence.replace(/Book /, '').trim() + if (updatedSequence.includes(' ')) { + updatedSequence = updatedSequence.split(' ').shift().replace(/,$/, '') + } + if (sequence !== updatedSequence) { + Logger.debug(`[Audible] Series "${seriesName}" sequence was cleaned from "${sequence}" to "${updatedSequence}"`) + } + return updatedSequence + } + cleanResult(item) { const { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language, runtimeLengthMin, formatType } = item @@ -25,13 +46,13 @@ class Audible { if (seriesPrimary) { series.push({ series: seriesPrimary.name, - sequence: (seriesPrimary.position || '').replace(/Book /, '') // Can be badly formatted see #1339 + sequence: this.cleanSeriesSequence(seriesPrimary.name, seriesPrimary.position || '') }) } if (seriesSecondary) { series.push({ series: seriesSecondary.name, - sequence: (seriesSecondary.position || '').replace(/Book /, '') + sequence: this.cleanSeriesSequence(seriesSecondary.name, seriesSecondary.position || '') }) } @@ -64,7 +85,7 @@ class Audible { } asinSearch(asin, region) { - asin = encodeURIComponent(asin); + asin = encodeURIComponent(asin) var regionQuery = region ? `?region=${region}` : '' var url = `https://api.audnex.us/books/${asin}${regionQuery}` Logger.debug(`[Audible] ASIN url: ${url}`) From 341a0452da4044fe8bec7745d8c54b28a5c5eb6b Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 7 Dec 2023 17:01:33 -0600 Subject: [PATCH 206/208] Update auth settings endpoint to return updated flag and show whether updates were made in client toast --- client/pages/config/authentication.vue | 8 ++++++-- server/controllers/MiscController.js | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index ffb1feb7a..9e0283071 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -202,7 +202,7 @@ export default { this.$toast.error('Mobile Redirect URIs: Asterisk (*) must be the only entry if used') isValid = false } else { - uris.forEach(uri => { + uris.forEach((uri) => { if (uri !== '*' && !isValidRedirectURI(uri)) { this.$toast.error(`Mobile Redirect URIs: Invalid URI ${uri}`) isValid = false @@ -230,7 +230,11 @@ export default { .$patch('/api/auth-settings', this.newAuthSettings) .then((data) => { this.$store.commit('setServerSettings', data.serverSettings) - this.$toast.success('Server settings updated') + if (data.updated) { + this.$toast.success('Server settings updated') + } else { + this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) + } }) .catch((error) => { console.error('Failed to update server settings', error) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index e209fac97..db4110e09 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -631,21 +631,25 @@ class MiscController { } } else if (key === 'authOpenIDMobileRedirectURIs') { function isValidRedirectURI(uri) { - const pattern = new RegExp('^\\w+://[\\w.-]+$', 'i'); - return pattern.test(uri); + if (typeof uri !== 'string') return false + const pattern = new RegExp('^\\w+://[\\w.-]+$', 'i') + return pattern.test(uri) } const uris = settingsUpdate[key] - if (!Array.isArray(uris) || - (uris.includes('*') && uris.length > 1) || - uris.some(uri => uri !== '*' && !isValidRedirectURI(uri))) { + if (!Array.isArray(uris) || + (uris.includes('*') && uris.length > 1) || + uris.some(uri => uri !== '*' && !isValidRedirectURI(uri))) { Logger.warn(`[MiscController] Invalid value for authOpenIDMobileRedirectURIs`) continue } // Update the URIs - Database.serverSettings[key] = uris - hasUpdates = true + if (Database.serverSettings[key].some(uri => !uris.includes(uri)) || uris.some(uri => !Database.serverSettings[key].includes(uri))) { + Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${Database.serverSettings[key]}" to "${uris}"`) + Database.serverSettings[key] = uris + hasUpdates = true + } } else { const updatedValueType = typeof settingsUpdate[key] if (['authOpenIDAutoLaunch', 'authOpenIDAutoRegister'].includes(key)) { @@ -688,6 +692,7 @@ class MiscController { } res.json({ + updated: hasUpdates, serverSettings: Database.serverSettings.toJSONForBrowser() }) } From 98104a3c03591af2c8b8885631ce5bf87c556682 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 7 Dec 2023 17:05:52 -0600 Subject: [PATCH 207/208] Map new translations to other files --- client/strings/cs.json | 2 ++ client/strings/da.json | 2 ++ client/strings/es.json | 2 ++ client/strings/fr.json | 2 ++ client/strings/gu.json | 2 ++ client/strings/hi.json | 2 ++ client/strings/hr.json | 2 ++ client/strings/it.json | 2 ++ client/strings/lt.json | 2 ++ client/strings/nl.json | 2 ++ client/strings/no.json | 2 ++ client/strings/pl.json | 2 ++ client/strings/ru.json | 2 ++ client/strings/sv.json | 2 ++ client/strings/zh-cn.json | 2 ++ 15 files changed, 30 insertions(+) diff --git a/client/strings/cs.json b/client/strings/cs.json index b89360245..6d39569e3 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -343,6 +343,8 @@ "LabelMinute": "Minuta", "LabelMissing": "Chybějící", "LabelMissingParts": "Chybějící díly", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Více", "LabelMoreInfo": "Více informací", "LabelName": "Jméno", diff --git a/client/strings/da.json b/client/strings/da.json index a93507c08..fa28dd244 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -343,6 +343,8 @@ "LabelMinute": "Minut", "LabelMissing": "Mangler", "LabelMissingParts": "Manglende dele", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Mere", "LabelMoreInfo": "Mere info", "LabelName": "Navn", diff --git a/client/strings/es.json b/client/strings/es.json index fc2f03164..473153015 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -343,6 +343,8 @@ "LabelMinute": "Minuto", "LabelMissing": "Ausente", "LabelMissingParts": "Partes Ausentes", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Más", "LabelMoreInfo": "Más Información", "LabelName": "Nombre", diff --git a/client/strings/fr.json b/client/strings/fr.json index f10a51f4e..f6efa428b 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -343,6 +343,8 @@ "LabelMinute": "Minute", "LabelMissing": "Manquant", "LabelMissingParts": "Parties manquantes", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Plus", "LabelMoreInfo": "Plus d’info", "LabelName": "Nom", diff --git a/client/strings/gu.json b/client/strings/gu.json index d65bb13e1..0317e2f98 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -343,6 +343,8 @@ "LabelMinute": "Minute", "LabelMissing": "Missing", "LabelMissingParts": "Missing Parts", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "More", "LabelMoreInfo": "More Info", "LabelName": "Name", diff --git a/client/strings/hi.json b/client/strings/hi.json index b172c2e51..eb4f074f9 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -343,6 +343,8 @@ "LabelMinute": "Minute", "LabelMissing": "Missing", "LabelMissingParts": "Missing Parts", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "More", "LabelMoreInfo": "More Info", "LabelName": "Name", diff --git a/client/strings/hr.json b/client/strings/hr.json index 50f384e74..eb7d27d85 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -343,6 +343,8 @@ "LabelMinute": "Minuta", "LabelMissing": "Nedostaje", "LabelMissingParts": "Nedostajali dijelovi", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Više", "LabelMoreInfo": "More Info", "LabelName": "Ime", diff --git a/client/strings/it.json b/client/strings/it.json index 638e3468a..7e5267217 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -343,6 +343,8 @@ "LabelMinute": "Minuto", "LabelMissing": "Altro", "LabelMissingParts": "Parti rimantenti", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Molto", "LabelMoreInfo": "Più Info", "LabelName": "Nome", diff --git a/client/strings/lt.json b/client/strings/lt.json index 3e3fda41b..9c4b9a63c 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -343,6 +343,8 @@ "LabelMinute": "Minutė", "LabelMissing": "Trūksta", "LabelMissingParts": "Trūkstamos dalys", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Daugiau", "LabelMoreInfo": "Daugiau informacijos", "LabelName": "Pavadinimas", diff --git a/client/strings/nl.json b/client/strings/nl.json index 088454887..d4779abd6 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -343,6 +343,8 @@ "LabelMinute": "Minuut", "LabelMissing": "Ontbrekend", "LabelMissingParts": "Ontbrekende delen", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Meer", "LabelMoreInfo": "Meer info", "LabelName": "Naam", diff --git a/client/strings/no.json b/client/strings/no.json index 8cbfd9197..511c8b865 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -343,6 +343,8 @@ "LabelMinute": "Minutt", "LabelMissing": "Mangler", "LabelMissingParts": "Manglende deler", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Mer", "LabelMoreInfo": "Mer info", "LabelName": "Navn", diff --git a/client/strings/pl.json b/client/strings/pl.json index bf34cbac1..b51084e94 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -343,6 +343,8 @@ "LabelMinute": "Minuta", "LabelMissing": "Brakujący", "LabelMissingParts": "Brakujące cześci", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Więcej", "LabelMoreInfo": "More Info", "LabelName": "Nazwa", diff --git a/client/strings/ru.json b/client/strings/ru.json index b0ba0f6a7..b48e0dbd2 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -343,6 +343,8 @@ "LabelMinute": "Минуты", "LabelMissing": "Потеряно", "LabelMissingParts": "Потерянные части", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Еще", "LabelMoreInfo": "Больше информации", "LabelName": "Имя", diff --git a/client/strings/sv.json b/client/strings/sv.json index 6883af399..fde0cd875 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -343,6 +343,8 @@ "LabelMinute": "Minut", "LabelMissing": "Saknad", "LabelMissingParts": "Saknade delar", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "Mer", "LabelMoreInfo": "Mer information", "LabelName": "Namn", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 14bfcc0bc..7c5594897 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -343,6 +343,8 @@ "LabelMinute": "分钟", "LabelMissing": "丢失", "LabelMissingParts": "丢失的部分", + "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", + "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMore": "更多", "LabelMoreInfo": "更多..", "LabelName": "名称", From 8d3d6363290aa820f7fd6e93c4d7bca91950d891 Mon Sep 17 00:00:00 2001 From: JBlond <leet31337@web.de> Date: Fri, 8 Dec 2023 09:39:04 +0100 Subject: [PATCH 208/208] Follow up for sso-redirecturi and #2305 #2333 8f4c65ec8c8838e71d7810266f60a85664927c27 / 7c9c278cc40acb2887f4d2b7b3c40241096ae38e sso-redirecturi 2f6756eddf03f758bea0f5dc7a154ba57ab1e69d #2333 2e5822b7c88ad362d7174c55becf320c0c834675 #2305 --- client/strings/de.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index da8af1d8a..3fa189607 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -87,9 +87,9 @@ "ButtonUserEdit": "Benutzer {0} bearbeiten", "ButtonViewAll": "Alles anzeigen", "ButtonYes": "Ja", - "ErrorUploadFetchMetadataAPI": "Error fetching metadata", - "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", - "ErrorUploadLacksTitle": "Must have a title", + "ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten", + "ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuchen Sie den Titel und oder den Autor zu updaten", + "ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden", "HeaderAccount": "Konto", "HeaderAdvanced": "Erweitert", "HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen", @@ -199,8 +199,8 @@ "LabelAuthorLastFirst": "Autor (Nachname, Vorname)", "LabelAuthors": "Autoren", "LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen", - "LabelAutoFetchMetadata": "Auto Fetch Metadata", - "LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.", + "LabelAutoFetchMetadata": "Automatisches Abholen der Metadaten", + "LabelAutoFetchMetadataHelp": "Abholen der Metadaten von Titel, Autor und Serien, um das Hochladen zu optimieren. Möglicherweise müssen zusätzliche Metadaten nach dem Hochladen abgeglichen werden.", "LabelAutoLaunch": "Automatischer Start", "LabelAutoLaunchDescription": "Automatische Weiterleitung zum Authentifizierungsanbieter beim Navigieren zur Anmeldeseite (manueller Überschreibungspfad <code>/login?autoLaunch=0</code>)", "LabelAutoRegister": "Automatische Registrierung", @@ -271,7 +271,7 @@ "LabelExample": "Beispiel", "LabelExplicit": "Explizit (Altersbeschränkung)", "LabelFeedURL": "Feed URL", - "LabelFetchingMetadata": "Fetching Metadata", + "LabelFetchingMetadata": "Abholen der Metadaten", "LabelFile": "Datei", "LabelFileBirthtime": "Datei erstellt", "LabelFileModified": "Datei geändert", @@ -289,7 +289,7 @@ "LabelHardDeleteFile": "Datei dauerhaft löschen", "LabelHasEbook": "mit E-Book", "LabelHasSupplementaryEbook": "mit zusätlichem E-Book", - "LabelHighestPriority": "Highest priority", + "LabelHighestPriority": "Höchste Priorität", "LabelHost": "Host", "LabelHour": "Stunde", "LabelIcon": "Symbol", @@ -331,12 +331,12 @@ "LabelLogLevelInfo": "Informationen", "LabelLogLevelWarn": "Warnungen", "LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum", - "LabelLowestPriority": "Lowest Priority", + "LabelLowestPriority": "Niedrigste Priorität", "LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit", "LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID von Ihrem SSO-Anbieter zugeordnet", "LabelMediaPlayer": "Mediaplayer", "LabelMediaType": "Medientyp", - "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", + "LabelMetadataOrderOfPrecedenceDescription": "Eine Höhere Priorität Quelle für Metadaten wird die Metadaten aus eine Quelle mit niedrigerer Priorität überschreiben.", "LabelMetadataProvider": "Metadatenanbieter", "LabelMetaTag": "Meta Schlagwort", "LabelMetaTags": "Meta Tags", @@ -523,7 +523,7 @@ "LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird", "LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern", "LabelUploaderDropFiles": "Dateien löschen", - "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", + "LabelUploaderItemFetchMetadataHelp": "Automatisches Abholden von Titel, Author und Serien", "LabelUseChapterTrack": "Kapiteldatei verwenden", "LabelUseFullTrack": "Gesamte Datei verwenden", "LabelUser": "Benutzer",