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 0001/2145] 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 8edcbc01..efddfd17 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 a8b1c69b..0cf19f48 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 2bca48d2..4cb40da6 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 a5a0b8b5..17cd6256 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 af0ce0e1..6e39f6f4 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 38b75370..9bdc7796 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 0002/2145] 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 4cb40da6..16889163 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 0003/2145] 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 82657aba..c32ea13b 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 3190539a..95429a08 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 16889163..40bb64ac 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 53e00e9b..602d8ac1 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 0004/2145] 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 a94045ae..8da68f5e 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 0bfa07d2..e87a4b5c 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 b5ee0bd1..43ab49e2 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 40bb64ac..9e1b2a12 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 d2865b88..96896a73 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 bf6ea6f7..a12bca29 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 00000000..d8ebfcf1 --- /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 00000000..365d4f65 --- /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 00000000..67110204 --- /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 602d8ac1..2a51e3de 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 0005/2145] 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 96896a73..1559a88e 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 679bdf36b117a59b4db0cd8022818acc5d17f5b7 Mon Sep 17 00:00:00 2001 From: Jorge <46056498+jorgectf@users.noreply.github.com> Date: Mon, 3 Jul 2023 09:15:04 +0200 Subject: [PATCH 0006/2145] Add CodeQL workflow --- .github/workflows/codeql.yml | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..a77ab3e0 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,65 @@ +name: "CodeQL" + +on: + push: + branches: [ 'master' ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ 'master' ] + schedule: + - cron: '16 5 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" 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 0007/2145] 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 52feedb6..27bdc63b 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 0885c88a..ceeddb36 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 9156c021..89985768 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 5c3c8715..81136ecf 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 6f457aa5..5e184fc7 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 0008/2145] 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 27bdc63b..1b17ab50 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 397babb5..967439f2 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 182c3033..8c765640 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 ceeddb36..a219129b 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 89985768..ad7fa16c 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 0009/2145] 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 a219129b..dc21aa37 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 0010/2145] 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 1b17ab50..beac44ff 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 0011/2145] 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 dc21aa37..c8376ff6 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 0012/2145] 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 f306a61d..7cc6fa76 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 0013/2145] 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 c8376ff6..0e62d7dd 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 0014/2145] 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 0e62d7dd..17fea247 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 0015/2145] 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 17fea247..d68ef876 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 7cc6fa76..cf55061d 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 81136ecf..28e59e40 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 0016/2145] cookie lifetime --- server/Auth.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index d68ef876..332e500d 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 0017/2145] 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 332e500d..f8074776 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 0018/2145] Add /auth_methods route --- server/Auth.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/Auth.js b/server/Auth.js index f8074776..15be664c 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 0019/2145] 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 beac44ff..29341ed3 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 0020/2145] 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 29341ed3..f9d82087 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 0021/2145] 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 15be664c..05044f74 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 cf55061d..2424456d 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 0022/2145] 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 05044f74..13d7cde9 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 0023/2145] 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 f9d82087..73cd2767 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 13d7cde9..0041fbed 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 2424456d..dbb9bddf 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 28e59e40..f1c69b24 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 0024/2145] 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 73cd2767..4ca7c302 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 0025/2145] 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 50e440d7..677aba70 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 542b7f2c..fdbd7150 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 00000000..acc0ac13 --- /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 2f8201c1..ed7c35b6 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 75606da2..47cfe448 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 0041fbed..d6d67d49 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 dbb9bddf..08f4b8d9 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 0fa1c62f..d1f3686b 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 71358a00..9348d691 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 71d9429e..d91c9312 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 0026/2145] 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 d6d67d49..b7ea59c4 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 08f4b8d9..2f04b850 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 85baeb27..884f0cd6 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 0027/2145] [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 96735cc9..debac709 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 0028/2145] [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 debac709..b30510f2 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 e10b1785651b672b9fcd50dcc7e9018e614fbd36 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 1 Oct 2023 09:03:01 -0500 Subject: [PATCH 0029/2145] Fix:Crash on failed scanner find covers #2164 --- server/scanner/BookScanner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index f8e959bd..187ecac1 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -1111,7 +1111,7 @@ class BookScanner { const result = await CoverManager.downloadCoverFromUrlNew(results[i], libraryItemId, libraryItemPath) if (result.error) { - Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error) + libraryScan.addLog(LogLevel.ERROR, `Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error) } else if (result.cover) { return result.cover } From 20a1d40d990b1d14750ba061a0dd808a016cb0ec Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 1 Oct 2023 12:44:52 -0500 Subject: [PATCH 0030/2145] Fix:Set date properly on local playback sessions #2168 --- server/controllers/LibraryItemController.js | 6 ------ server/objects/PlaybackSession.js | 8 +++++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 498398f7..4aff7a13 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -280,12 +280,6 @@ class LibraryItemController { return CacheManager.handleCoverCache(res, libraryItem.id, libraryItem.media.coverPath, options) } - // GET: api/items/:id/stream - openStream(req, res) { - // this.streamManager.openStreamApiRequest(res, req.user, req.libraryItem) - res.sendStatus(500) - } - // POST: api/items/:id/play startPlaybackSession(req, res) { if (!req.libraryItem.media.numTracks && req.libraryItem.mediaType !== 'video') { diff --git a/server/objects/PlaybackSession.js b/server/objects/PlaybackSession.js index 4fe3d8fd..20b50009 100644 --- a/server/objects/PlaybackSession.js +++ b/server/objects/PlaybackSession.js @@ -168,7 +168,13 @@ class PlaybackSession { this.currentTime = session.currentTime || 0 this.startedAt = session.startedAt - this.updatedAt = session.updatedAt || null + this.updatedAt = session.updatedAt || session.startedAt + + // Local playback sessions dont set this date field so set using updatedAt + if (!this.date && session.updatedAt) { + this.date = date.format(new Date(session.updatedAt), 'YYYY-MM-DD') + this.dayOfWeek = date.format(new Date(session.updatedAt), 'dddd') + } } get mediaItemId() { From 73bb73a04aa04fb123a9ff1ef8e96e3ce5c2201e Mon Sep 17 00:00:00 2001 From: Alistair Bahr Date: Mon, 2 Oct 2023 09:25:34 +0200 Subject: [PATCH 0031/2145] make force transcode apply to all "ffmpeg error 1" --- server/objects/Stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/objects/Stream.js b/server/objects/Stream.js index c8452ac3..1860c47f 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -340,7 +340,7 @@ class Stream extends EventEmitter { Logger.error('Ffmpeg Err', '"' + err.message + '"') // Temporary workaround for https://github.com/advplyr/audiobookshelf/issues/172 - const aacErrorMsg = 'ffmpeg exited with code 1: Could not write header for output file #0 (incorrect codec parameters ?)' + const aacErrorMsg = 'ffmpeg exited with code 1:' if (audioCodec === 'copy' && this.isAACEncodable && err.message && err.message.startsWith(aacErrorMsg)) { Logger.info(`[Stream] Re-attempting stream with AAC encode`) this.transcodeOptions.forceAAC = true From 4352989242bf9a25b092deafb7c50b5e17a0eadc Mon Sep 17 00:00:00 2001 From: Alistair1231 Date: Mon, 2 Oct 2023 09:30:57 +0200 Subject: [PATCH 0032/2145] update comment to include second issue that is adressed by change --- server/objects/Stream.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/objects/Stream.js b/server/objects/Stream.js index 1860c47f..9f5ff59b 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -339,7 +339,7 @@ class Stream extends EventEmitter { } else { Logger.error('Ffmpeg Err', '"' + err.message + '"') - // Temporary workaround for https://github.com/advplyr/audiobookshelf/issues/172 + // Temporary workaround for https://github.com/advplyr/audiobookshelf/issues/172 and https://github.com/advplyr/audiobookshelf/issues/2157 const aacErrorMsg = 'ffmpeg exited with code 1:' if (audioCodec === 'copy' && this.isAACEncodable && err.message && err.message.startsWith(aacErrorMsg)) { Logger.info(`[Stream] Re-attempting stream with AAC encode`) @@ -435,4 +435,4 @@ class Stream extends EventEmitter { return newAudioTrack } } -module.exports = Stream \ No newline at end of file +module.exports = Stream From 7c9631c1b0dbff6e15a33c72945ef69449225e0d Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 2 Oct 2023 08:34:56 -0500 Subject: [PATCH 0033/2145] Update server/objects/Stream.js --- server/objects/Stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/objects/Stream.js b/server/objects/Stream.js index 9f5ff59b..115bb96e 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -341,7 +341,7 @@ class Stream extends EventEmitter { // Temporary workaround for https://github.com/advplyr/audiobookshelf/issues/172 and https://github.com/advplyr/audiobookshelf/issues/2157 const aacErrorMsg = 'ffmpeg exited with code 1:' - if (audioCodec === 'copy' && this.isAACEncodable && err.message && err.message.startsWith(aacErrorMsg)) { + if (audioCodec === 'copy' && this.isAACEncodable && err.message?.startsWith(aacErrorMsg)) { Logger.info(`[Stream] Re-attempting stream with AAC encode`) this.transcodeOptions.forceAAC = true this.reset(this.startTime) From a3a8937ba37b439dd40c406005c4bb280af0b12c Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 2 Oct 2023 17:09:12 -0500 Subject: [PATCH 0034/2145] Fix:Crash when searching for cover without an author #2174 --- server/controllers/LibraryItemController.js | 1 - server/controllers/SearchController.js | 2 +- server/finders/BookFinder.js | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 4aff7a13..2b25474e 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -259,7 +259,6 @@ class LibraryItemController { // Check if library item media has a cover path if (!libraryItem.media.coverPath || !await fs.pathExists(libraryItem.media.coverPath)) { - Logger.debug(`[LibraryItemController] getCover: Library item "${req.params.id}" has no cover path`) return res.sendStatus(404) } diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index 2749016c..93587bc4 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -26,7 +26,7 @@ class SearchController { let results = null if (podcast) results = await PodcastFinder.findCovers(query.title) - else results = await BookFinder.findCovers(query.provider || 'google', query.title, query.author || null) + else results = await BookFinder.findCovers(query.provider || 'google', query.title, query.author || '') res.json({ results }) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 96735cc9..bcf8ea06 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -234,7 +234,7 @@ class BookFinder { if (!books.length && maxFuzzySearches > 0) { // normalize title and author title = title.trim().toLowerCase() - author = author.trim().toLowerCase() + author = author?.trim().toLowerCase() || '' // Now run up to maxFuzzySearches fuzzy searches let candidates = new Set() From 733ad52684cfa0b88e6d7aa33d31a01f87f374e3 Mon Sep 17 00:00:00 2001 From: MarshDeer Date: Mon, 2 Oct 2023 19:42:25 -0300 Subject: [PATCH 0035/2145] 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 ae018d4b..15812766 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 0036/2145] 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 b872ca4e..d2ea30c7 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 0037/2145] [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 b30510f2..aa66fb92 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 0038/2145] 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 dd782d30..77619e55 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 0c5d67eb..1c682919 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 439f65c5..495a14ef 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 0039/2145] [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 aa66fb92..6ca238ee 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 0040/2145] [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 6ca238ee..1fe86718 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 0041/2145] 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 a09de35d..ddad3cd3 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 cfcf3f47..863012b5 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 a3f70e20..2695a7a0 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 6f457aa5..bf22a3a5 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 1ed74bb2..a9c9c767 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 0042/2145] [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 1fe86718..2bd1c571 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 0043/2145] [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 2bd1c571..b29417cb 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 0044/2145] [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 b29417cb..8876e2bd 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 0045/2145] [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 8876e2bd..70031fa3 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 0046/2145] [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 70031fa3..e3e87f4a 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 0047/2145] [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 e3e87f4a..d3192142 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 0048/2145] [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 d3192142..8c420333 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 4e6b75d6506d94c19fad81ae62cbac0c3370fd1e Mon Sep 17 00:00:00 2001 From: jfrazx Date: Thu, 5 Oct 2023 13:48:55 -0700 Subject: [PATCH 0049/2145] fix; HTTP/429 when requesting authors information, resolves #1570 --- package-lock.json | 29 ++++++++- package.json | 3 +- server/providers/Audnexus.js | 117 ++++++++++++++++++++++++----------- 3 files changed, 111 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77948004..a1e7ccd6 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", + "limiter": "^2.1.0", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "sequelize": "^6.32.1", @@ -1308,6 +1309,19 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "node_modules/just-performance": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/just-performance/-/just-performance-4.3.0.tgz", + "integrity": "sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q==" + }, + "node_modules/limiter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-2.1.0.tgz", + "integrity": "sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw==", + "dependencies": { + "just-performance": "4.3.0" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -3673,6 +3687,19 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "just-performance": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/just-performance/-/just-performance-4.3.0.tgz", + "integrity": "sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q==" + }, + "limiter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-2.1.0.tgz", + "integrity": "sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw==", + "requires": { + "just-performance": "4.3.0" + } + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -4672,4 +4699,4 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 4a00fa59..082006e8 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "express": "^4.17.1", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", + "limiter": "^2.1.0", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "sequelize": "^6.32.1", @@ -44,4 +45,4 @@ "devDependencies": { "nodemon": "^2.0.20" } -} \ No newline at end of file +} diff --git a/server/providers/Audnexus.js b/server/providers/Audnexus.js index b74d1d13..06433f5d 100644 --- a/server/providers/Audnexus.js +++ b/server/providers/Audnexus.js @@ -1,78 +1,123 @@ const axios = require('axios') const { levenshteinDistance } = require('../utils/index') const Logger = require('../Logger') +const { RateLimiter } = require('limiter'); class Audnexus { + static _instance = null; + constructor() { + // ensures Audnexus class is singleton + if (Audnexus._instance) { + return Audnexus._instance + } + this.baseUrl = 'https://api.audnex.us' + + // @see https://github.com/laxamentumtech/audnexus#-deployment- + this.limiter = new RateLimiter({ + tokensPerInterval: 100, + fireImmediately: true, + interval: 'minute', + }) + + Audnexus._instance = this } authorASINsRequest(name, region) { name = encodeURIComponent(name) const regionQuery = region ? `®ion=${region}` : '' const authorRequestUrl = `${this.baseUrl}/authors?name=${name}${regionQuery}` + Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - return axios.get(authorRequestUrl).then((res) => { - return res.data || [] - }).catch((error) => { - Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error) - return [] - }) + + return this._processRequest(() => axios.get(authorRequestUrl)) + .then((res) => res.data || []) + .catch((error) => { + Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error) + return [] + }) } authorRequest(asin, region) { asin = encodeURIComponent(asin) const regionQuery = region ? `?region=${region}` : '' const authorRequestUrl = `${this.baseUrl}/authors/${asin}${regionQuery}` + Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - return axios.get(authorRequestUrl).then((res) => { - return res.data - }).catch((error) => { - Logger.error(`[Audnexus] Author request failed for ${asin}`, error) - return null - }) + + return this._processRequest(() => axios.get(authorRequestUrl)) + .then((res) => res.data) + .catch((error) => { + Logger.error(`[Audnexus] Author request failed for ${asin}`, error) + return null + }) + } + + /** + * @description Process a request with a rate limiter + * + * @param {*} request + * @returns + */ + async _processRequest(request) { + const remainingTokens = await this.limiter.removeTokens(1) + Logger.info(`[Audnexus] Attempting request with ${remainingTokens} remaining tokens and ${this.limiter.tokensThisInterval} this interval`) + + if (remainingTokens >= 1) { + return request() + } + + // 100 tokens(requests) per minute give a refresh of ~1.67 per second, + // so a 10 second wait will yield ~16.7 additional tokens + Logger.info('[Audnexus] Sleeping for 10 seconds') + await new Promise(resolve => setTimeout(resolve, 10000)) + + return this._processRequest(request) } async findAuthorByASIN(asin, region) { const author = await this.authorRequest(asin, region) - if (!author) { - return null - } - return { - asin: author.asin, - description: author.description, - image: author.image || null, - name: author.name - } + + return author ? + { + asin: author.asin, + description: author.description, + image: author.image || null, + name: author.name + } : null } async findAuthorByName(name, region, maxLevenshtein = 3) { Logger.debug(`[Audnexus] Looking up author by name ${name}`) + const asins = await this.authorASINsRequest(name, region) const matchingAsin = asins.find(obj => levenshteinDistance(obj.name, name) <= maxLevenshtein) + if (!matchingAsin) { return null } + const author = await this.authorRequest(matchingAsin.asin) - if (!author) { - return null - } - return { - asin: author.asin, - description: author.description, - image: author.image || null, - name: author.name - } + return author ? + { + description: author.description, + image: author.image || null, + asin: author.asin, + name: author.name + } : null } getChaptersByASIN(asin, region) { Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}/${region}`) - return axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`).then((res) => { - return res.data - }).catch((error) => { - Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error) - return null - }) + + return this._processRequest(() => axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`)) + .then((res) => res.data) + .catch((error) => { + Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error) + return null + }) } } + module.exports = Audnexus \ No newline at end of file From b447cf5c1ccb273fd45af8a7e8b11f3844c28fe6 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 5 Oct 2023 17:00:40 -0500 Subject: [PATCH 0050/2145] 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 f768bb93..b25e02aa 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 5797b0b5..abcc626c 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 10e1101d..d23459b4 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 27ac3fcd..7665c89b 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 0051/2145] 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 f2c542eb..4fa8e394 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 fb9528ce..c86d3228 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 0052/2145] 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 54ac63a4..a0b64f55 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 0053/2145] 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 633b7646..1fd011cf 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 00000000..95ae801a --- /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 b84dec44..cfb30a0c 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 67391141..936f6a30 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 ccd42ede..b72df02f 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 37478bf0..9195265e 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 f7d548ba..f03c6352 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 5d0e4b3a..031462b2 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 5018cf4d..0803ccf4 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 21ed9893..1eea8495 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 b0e0db91..47908b18 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 96a24392..b60a87c1 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 4f7bf2ed..31d259e6 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 ac61de96..eb6b35b3 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 d1f51aac..f4fe316c 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 c4e6ae84..a645877b 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 3c95affa..f7f56965 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 09eb6708..1d7f90dd 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 77948004..7178ac98 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 4a00fa59..e76147d8 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 ac019a96..b0ecf446 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 f30c9c6d..934deaff 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 966c7a93..37e89029 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 0062/2145] 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 3af64249..a4fb48a2 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 0cd243fd..62a7ebde 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 9c2a3b4f..59c6ce16 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 dc816b44..a90d1873 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 0063/2145] 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 1d7f90dd..f5a32ff4 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 0064/2145] 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 59c6ce16..69aa724d 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 934deaff..3cf97f33 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 37e89029..4df26400 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 0065/2145] 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 115bb96e..2ee66182 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 d4f1bc60..711e360a 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 0066/2145] 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 d9d077c0..037726f6 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 c272127f..576280c8 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 df6639e0..21c28b8c 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 0067/2145] 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 5f193d7b..f404bb80 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 00000000..359ecdd6 --- /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 0068/2145] 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 e9ca3302..588b7744 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 64977e2d..44ccdd05 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 0069/2145] 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 61cfa715..c9834d8d 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 0070/2145] 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 0f75644b..1378dbe5 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 c4112db6..22c3cafa 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 5dec2152..b88a38af 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 6416627a..55b2f9d4 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 2b91aeb6..0a8f3349 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 2fd684ea..0e68a0a4 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 0071/2145] 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 1fd011cf..09c0fc1d 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 00000000..d1e62dd4 --- /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 2b76d6b3..9c593ff2 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 a90d1873..03a0696c 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 0072/2145] 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 09c0fc1d..03b66931 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 d1e62dd4..7297c1ae 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 0073/2145] 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 e579bcc9..f752417c 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 0074/2145] 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 d40ce0da..1aecbf4e 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 c06c5333..db4e7e9a 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 834088d9..399c429a 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 ff986a33..e5486461 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 c9834d8d..5ef1bef2 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 176725b9..43adc727 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 0075/2145] 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 b72df02f..50d0dbeb 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 0076/2145] 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 43adc727..7e0bc3dd 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 0077/2145] 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 d1d36d0b..36780df4 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 83b1776e..00000000 --- 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 335a52a1..00000000 --- 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 e36a5a92..bb91e2d6 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 0a8f3349..1452b7b5 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 33cbc016..afbf1622 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 7512c4ec..d4b8a518 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 9ddb3412..969e2548 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 dae834c1..940eab0b 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 9fb07bc8..490b9949 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 7da47314..90a887e0 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 2c371c6c..8300e93a 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 15d57fbe..a2194d15 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 03a0696c..034951df 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 e638a9c5..00000000 --- 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 07ab5ccd..00000000 --- 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 0078/2145] 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 879d50a3..92599c7a 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 d51ed208..d3f956ec 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 7e0bc3dd..0f4f17b2 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 359ecdd6..905beb26 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 50d0dbeb..7bcc2df9 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 9195265e..6befd10d 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 f03c6352..57d09a72 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 031462b2..25b3261e 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 0803ccf4..7716773d 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 1eea8495..3cc25ae6 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 47908b18..1a97f0f4 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 b60a87c1..003e167c 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 31d259e6..3266e978 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 eb6b35b3..da0b8046 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 f4fe316c..90e8758f 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 a645877b..82167c08 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 f7f56965..d4d258d3 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 f5a32ff4..b76e7949 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 0079/2145] 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 36780df4..f5bb85a0 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 0fa1c62f..ffa4e2c2 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 4ced1390..8a87df2e 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 2f74bcbe..11c82822 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 b88a38af..7b6ff845 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 38e8b580..747ded08 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 04c83d17..db7e490e 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 034951df..b40c3d80 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 0080/2145] 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 63a3644d..c9de1a87 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 2817f23f..5ff34439 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 7b6ff845..6e91a0aa 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 747ded08..31cf06a1 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 88562e2c..1dc945fb 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 44ccdd05..b7f9093e 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 abcc626c..84167229 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 0081/2145] 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 25b581c9..8dd3e260 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 577460a4..3ce6a5f5 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 b7f9093e..11a88bd4 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 0082/2145] 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 8dd3e260..6a0cb36f 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 5ff34439..1f2acbd3 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 9a330f4f..de154c3d 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 e8422c77..9277d412 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 f5bb85a0..d95bd799 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 86f94d3d..ea84e7df 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 0083/2145] 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 03b66931..5bcdabed 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 0084/2145] 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 c9de1a87..d284c505 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 14fe68a7..62f08c92 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 598b12b7..faf8d69d 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 1f2acbd3..4f5e0fea 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 de154c3d..ccdc1791 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 9277d412..96e7e5b8 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 9c593ff2..10a77b2a 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 d17dbd15..b6f2f285 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 ebce3607..616baf29 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 0085/2145] 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 7bcc2df9..67dc2930 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 0086/2145] 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 77619e55..f2c15280 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 936f6a30..1721a379 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 0087/2145] 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 95ae801a..215f79b5 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 905beb26..aa1b66ed 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 67dc2930..fe1df71c 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 6befd10d..43f44821 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 57d09a72..3a6012b6 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 25b3261e..a78f7d66 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 7716773d..8b6a963f 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 3cc25ae6..7c8651e3 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 1a97f0f4..8e3946a5 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 003e167c..e384ea59 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 3266e978..c5c937ba 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 da0b8046..a2046d57 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 90e8758f..26a282af 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 82167c08..b38406ba 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 d4d258d3..94a8bc63 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 b76e7949..0ea26727 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 0088/2145] 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 cc785926..21cae124 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 1721a379..12ce7b1e 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 aa1b66ed..adf138a1 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 fe1df71c..a072a549 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 43f44821..24d07726 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 3a6012b6..4b37139d 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 a78f7d66..28bdf743 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 8b6a963f..8593a95d 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 7c8651e3..82d25986 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 8e3946a5..e9a323ee 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 e384ea59..f73b3ffc 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 c5c937ba..dee54e12 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 a2046d57..62696dce 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 26a282af..dc7685ee 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 b38406ba..c4fb50f8 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 94a8bc63..69868bca 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 0ea26727..219e861a 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 e76147d8..f8ea7dee 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 521e016d..5721ac27 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 31bcfa3c..9537d7b3 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 60311bfd..82ae8fe2 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 bb91e2d6..3b92bdcc 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 afbf1622..d53a53a7 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 969e2548..a0e5de04 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 5c0d9dad..f31aaf6b 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 037726f6..1f9d2823 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 f752417c..282155f2 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 53d4ad1f..b56c4db6 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 ff82ac33..e0b78d2e 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 00000000..0d9f909a --- /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 0089/2145] 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 d3f956ec..1b87df0f 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 0090/2145] 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 50e440d7..267aabaa 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 995f4c23..deb96a6c 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 a9a61e1b..00000000 --- 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 4f5e0fea..df8f754a 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 0091/2145] 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 3ce6a5f5..f348ce8e 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 4df26400..7ee16d56 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 0092/2145] 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 7ee16d56..19735fb7 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 0093/2145] 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 1aecbf4e..3439910f 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 be39ae3c..810baa43 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 0f4f17b2..eff240f7 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 a202d685..44b35f88 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 0094/2145] 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 ffa4e2c2..fb6124df 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 b40c3d80..c4ac0327 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 0095/2145] 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 10a77b2a..d2090270 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 6ba6ec5e..785124a9 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 0096/2145] 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 f348ce8e..99318a7e 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 19735fb7..26578f57 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 0097/2145] 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 fba30ec9..7cc3c33a 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 120bb400..569ff84f 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 adf138a1..3197cc3c 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 a072a549..942cad8b 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 24d07726..9e69aa4e 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 4b37139d..b04815ab 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 28bdf743..11fa1468 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 8593a95d..b3de487a 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 82d25986..d05c1e85 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 dee54e12..0623a7ab 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 62696dce..659e3ec5 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 dc7685ee..5bf537f2 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 c4fb50f8..16a0970b 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 69868bca..478ac33a 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 219e861a..ded2c9e2 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 0098/2145] 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 e9a323ee..32213095 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 f73b3ffc..8de1f4ec 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 0099/2145] 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 fb6124df..f4f1703d 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 c4ac0327..41b24716 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 0100/2145] 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 7cc3c33a..11e7bf9e 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 569ff84f..2a7b90cf 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 c347eea3..56825491 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 0101/2145] 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 11e7bf9e..aa11d162 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 0102/2145] 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 3439910f..e9b6969d 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 810baa43..a2a4cc2f 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 00000000..b8147aa7 --- /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 df8f754a..c3cc3484 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 eff240f7..657d564d 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 44b35f88..961dd52e 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 0103/2145] 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 233839e7..c78873e3 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 ddad3cd3..bdb8711d 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 863012b5..5494911f 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 5ae659fa..e161a583 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 73158ab1..1293ccab 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 28dba670..813e69ec 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 482da3ce..a03e2655 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 3197cc3c..cf9f836b 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 942cad8b..e9242a3e 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 9e69aa4e..bfaac5ea 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 b04815ab..ca659fc8 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 11fa1468..be624142 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 b3de487a..eb24cd6a 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 d05c1e85..4ffa2bd3 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 32213095..71090fe1 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 8de1f4ec..88f3c5b7 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 0623a7ab..6e85d689 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 659e3ec5..9391f332 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 5bf537f2..ac16d351 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 16a0970b..b92cb894 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 478ac33a..5574aa9e 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 ded2c9e2..fa815fab 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 0104/2145] 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 20b1c908..da856de7 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 0105/2145] 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 2a7b90cf..5ee85182 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 56825491..5f871635 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 0106/2145] 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 1378dbe5..4a1b4753 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 0107/2145] 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 0e68a0a4..cf1567f9 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 0108/2145] 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 891c13c3..fd883151 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 0109/2145] 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 4b6e87cf..79d80f7c 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 69f04afe..58155499 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 1d4018fb..852aa997 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 3baac572..7a3c7f00 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 cf9f836b..768bb724 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 e9242a3e..f7cf8b68 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 bfaac5ea..1366c762 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 ca659fc8..0ac0a960 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 be624142..5ad80723 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 eb24cd6a..d71c9f17 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 4ffa2bd3..51b2e762 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 71090fe1..e04343a0 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 88f3c5b7..747d7420 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 6e85d689..ebc6b558 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 9391f332..06aed904 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 ac16d351..7fcd1c96 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 b92cb894..dd3c1d4a 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 5574aa9e..832ffe8b 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 fa815fab..5d3de27a 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 fefc23b6..fcbc4905 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 40648887..81e31d53 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 a9c9c767..5192752a 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 41b24716..bb91e9b5 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 0110/2145] 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 25000ab0..1dc72e4c 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 21cae124..c815d388 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 7178ac98..888c3beb 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 f8ea7dee..4bef0e42 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 0111/2145] 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 a0b64f55..75e5a5f1 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 84167229..0377b173 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 0112/2145] 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 eeef5379..b87cf88f 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 0113/2145] 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 d95bd799..ba63b2bd 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 da856de7..08a602ae 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 0114/2145] 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 00000000..06e01946 --- /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 0115/2145] 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 0116/2145] 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 f404bb80..9a7eb02e 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 06e01946..0adbfcc7 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 0117/2145] 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 0adbfcc7..07f3d4f7 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 0118/2145] 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 acc0ac13..317364a2 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 4ca7c302..724f5999 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 08f206dc..6c6a17b0 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 301e38e4..71e2e05d 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 0119/2145] 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 c44ad70d..6d8f6666 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 0120/2145] 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 11a88bd4..27c507bd 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 0121/2145] 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 317364a2..13867ef3 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 55149176..dd2b8339 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 a3b4a7c5..d4e9c209 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 b7ea59c4..fa5020a0 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 71e2e05d..781943b4 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 0122/2145] 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 75e5a5f1..1af3c0a3 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 0123/2145] 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 1af3c0a3..f5f150d2 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 0124/2145] 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 888c3beb..650850c6 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 4bef0e42..c21a12f6 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 0125/2145] 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 f5f150d2..212c588a 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 0126/2145] 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 212c588a..e3a84be0 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 0127/2145] 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 00000000..f0580847 --- /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 0128/2145] 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 e3a84be0..fa034bce 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 0129/2145] 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 9a7eb02e..ea6a06db 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 f0580847..23d489d0 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 0130/2145] 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 00000000..f0580847 --- /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 0131/2145] 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 9a7eb02e..ea6a06db 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 f0580847..23d489d0 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 0132/2145] 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 fa5020a0..a04f9ac4 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 0133/2145] 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 6c6a17b0..df6c9003 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 0134/2145] 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 13867ef3..7cedfd25 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 a04f9ac4..361380f8 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 0135/2145] 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 399c429a..ed216bb3 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 0136/2145] 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 00000000..ab82b0f9 --- /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 0137/2145] 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 ed216bb3..110edcb9 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 0138/2145] 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 d71c9f17..9a105722 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 0139/2145] 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 9a105722..3504916e 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 0140/2145] 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 3504916e..ae57c7a2 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 0141/2145] 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 fa034bce..ac3de2a7 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 0142/2145] 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 ac3de2a7..7d26b6bf 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 0143/2145] 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 ab82b0f9..c204b99a 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 0144/2145] 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 c204b99a..2c54c880 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 0145/2145] 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 7cedfd25..0da486c1 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 781943b4..05a64d06 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 0146/2145] 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 361380f8..eeb7ad47 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 d3028d9a..4c348f42 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 5192752a..b503872d 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 0147/2145] 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 db4e7e9a..fc3bc4b2 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 0148/2145] 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 747d7420..c893212e 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 0149/2145] 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 650850c6..c0f10bb8 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 c21a12f6..62e1eb2e 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 2c54c880..00000000 --- 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 00000000..01dcb575 --- /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 0150/2145] 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 4081f98c..7be286fe 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 0c85774c..3532c2a7 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 c6c8c777..fef1ae5a 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 2554fff1..5730ee36 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 0151/2145] 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 92599c7a..c281f821 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 1b87df0f..c4d1345d 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 657d564d..8658a6e4 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 0152/2145] Remove test/ from .gitigore (now contains unit tests) --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6f47029b..fbea7c5f 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 0153/2145] 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 fbea7c5f..9360600a 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 c0f10bb8..58ed3695 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 62e1eb2e..32b3840b 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 0154/2145] 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 0da486c1..9ea8172a 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 eeb7ad47..15e36576 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 9b58188f..6d7507cf 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 05a64d06..afde1ddf 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 c4c0df5e..8c97d59b 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 0155/2145] 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 9b79bc0f..ab33a5b3 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 deb96a6c..56207526 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 0156/2145] 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 15e36576..54fa52a4 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 0157/2145] 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 b7b8499d..1a83dc1c 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 d9b75715..7f73a956 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 724f5999..6ec67b00 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 dd2b8339..b068fe8e 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 d4e9c209..6e283cdc 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 54fa52a4..f504e315 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 0158/2145] 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 6ec67b00..f7579dd6 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 f504e315..06db47a8 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 6d7507cf..11adf3e9 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 afde1ddf..df5e71f1 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 0159/2145] 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 5bcdabed..2a68dd63 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 215f79b5..253d4e6b 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 b734b6bf..10ee19e0 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 282155f2..48e8529a 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 576280c8..b604e4d7 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 00000000..e450b5c3 --- /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 00000000..a7fbbceb --- /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 0160/2145] 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 00000000..9d80fdb2 --- /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 0161/2145] 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 00000000..af019e78 --- /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 0162/2145] 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 ba63b2bd..f90e9754 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 bb91e9b5..43c32628 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 0163/2145] 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 888c3beb..fedfddc6 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 4bef0e42..dafd8907 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 0164/2145] 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 2fb8b48f..397b9618 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 01dcb575..2728f174 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 0165/2145] 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 06db47a8..4c7b8d21 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 df6c9003..1397bbd1 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 0166/2145] 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 4c7b8d21..261c0854 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 11adf3e9..267db5c8 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 df5e71f1..bf3db557 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 0167/2145] 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 19e657b4..5eb33a24 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 0168/2145] 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 261c0854..a0f791ca 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 0169/2145] 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 a0f791ca..76fbc66b 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 0170/2145] 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 19e657b4..9909778f 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 0171/2145] 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 21d97b20..68b77d56 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 09a9008b..bf3cf584 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 6f06ca77..9c608e48 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 0172/2145] 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 bf3cf584..8dd13990 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 0173/2145] 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 76fbc66b..e2053fa5 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 0174/2145] 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 00000000..91141335 --- /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 0175/2145] 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 f7cf8b68..2b1db3ae 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 0176/2145] 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 9d80fdb2..882b9b61 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 43c32628..e499b2cf 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 0177/2145] 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 6f47029b..9360600a 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 2fb8b48f..397b9618 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 b7b8499d..1a83dc1c 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 267aabaa..c2db0725 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 d9b75715..7f73a956 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 542b7f2c..fdbd7150 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 00000000..9ea8172a --- /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 f1e58d33..f7579dd6 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 2f8201c1..ed7c35b6 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 1366c762..6f06ca77 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 fedfddc6..e1a5f266 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 dafd8907..477f62af 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 6c7b9891..e2053fa5 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 19e657b4..5eb33a24 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 f90e9754..69ccd257 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 ea84e7df..31012107 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 f4f1703d..267db5c8 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 85baeb27..884f0cd6 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 2695a7a0..86d2c78e 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 75e5a5f1..7d26b6bf 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 00000000..d8ebfcf1 --- /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 00000000..365d4f65 --- /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 00000000..67110204 --- /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 bf22a3a5..4c348f42 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 f31aaf6b..bf3db557 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 5192752a..b503872d 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 e499b2cf..b1888295 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 00000000..2728f174 --- /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 0178/2145] 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 1397bbd1..e7be5492 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 0179/2145] 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 9ea8172a..a71f62e8 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 0180/2145] 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 a71f62e8..e2f6d678 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 07f3d4f7..26e7bcc9 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 768bb724..d950be16 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 f7cf8b68..cb399ca4 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 6f06ca77..b7896281 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 0ac0a960..fde3782e 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 5ad80723..97d2766e 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 d71c9f17..3fca0367 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 51b2e762..1f35e11a 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 e04343a0..5fe09ab2 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 c893212e..881cc19b 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 ebc6b558..00d3aeed 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 06aed904..1421368b 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 7fcd1c96..7d2acf3b 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 dd3c1d4a..b0521f0a 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 832ffe8b..851d2ba3 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 23d489d0..eea30043 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 5d3de27a..8bb242a4 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 0181/2145] 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 882b9b61..b311af53 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 00000000..2cfc0dfc --- /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 0182/2145] 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 b1888295..d7714568 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 0183/2145] 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 9ccd829b..77c004e8 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 af019e78..6945a706 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 0184/2145] 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 e2053fa5..dedf32f0 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 0185/2145] 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 0186/2145] 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 31012107..da17f5df 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 4c348f42..220c0c40 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 0187/2145] 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 fbed11be..15f34867 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 0188/2145] 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 6945a706..00000000 --- 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 0189/2145] 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 fcb1cd33..6e86b3ac 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 0190/2145] 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 b311af53..c6579ab3 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 2cfc0dfc..dc1ee1ed 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 0191/2145] 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 253d4e6b..8ec73dd0 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 07f3d4f7..71d06dca 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 768bb724..3fafae9f 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 f7cf8b68..f957ca99 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 6f06ca77..8d9a22c8 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 0ac0a960..d4752c1a 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 5ad80723..56f214a2 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 d71c9f17..6e11a221 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 51b2e762..6e228c89 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 e04343a0..6c24b1bd 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 c893212e..d5cee73c 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 ebc6b558..520aaa74 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 06aed904..e05db32b 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 7fcd1c96..b36a0026 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 dd3c1d4a..e3729a6b 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 832ffe8b..9caabb72 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 23d489d0..ab20e40b 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 5d3de27a..616e4f6c 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 0192/2145] 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 a7fbbceb..ac41cfe6 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 0193/2145] 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 ac41cfe6..56e9400a 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 0194/2145] 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 91141335..70e6a096 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 0195/2145] 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 e2f6d678..e645569e 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 0196/2145] 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 1dc72e4c..16adf9db 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 c815d388..a13b5815 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 e1a5f266..9df54fdd 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 477f62af..061e2a7f 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 0197/2145] 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 dedf32f0..af2b4289 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 0198/2145] 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 af2b4289..74ccf240 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 0199/2145] 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 68b77d56..a393a170 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 8dd13990..e29239a0 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 78049f24..5b73de36 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 0200/2145] 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 a393a170..4a40f2b7 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 00000000..41a28b4d --- /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 5b73de36..857627e9 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 0201/2145] 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 74ccf240..96a2bc9e 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 0202/2145] Auto formatting --- server/Auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 96a2bc9e..57792177 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 0203/2145] 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 4883fb71..5e8cab76 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 80458e24bd94357ccc6178ac4ce0ecde8c6d47c1 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 30 Nov 2023 21:15:25 +0200 Subject: [PATCH 0204/2145] "[un]abridged" in title candidate generation --- server/finders/BookFinder.js | 1 + test/server/finders/BookFinder.test.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 7d26b6bf..0c5f32d2 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -167,6 +167,7 @@ class BookFinder { [/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''], // Remove edition [/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type [/ a novel.*$/g, ''], // Remove "a novel" + [/(^| )(un)?abridged( |$)/g, ' '], // Remove "unabridged/abridged" [/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers ] diff --git a/test/server/finders/BookFinder.test.js b/test/server/finders/BookFinder.test.js index 2728f174..ed2442c6 100644 --- a/test/server/finders/BookFinder.test.js +++ b/test/server/finders/BookFinder.test.js @@ -35,6 +35,8 @@ describe('TitleCandidates', () => { ['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 "abridged"', 'abridged anna karenina', ['anna karenina', 'abridged anna karenina']], + ['adds candidate + variant, removing "unabridged"', 'anna karenina unabridged', ['anna karenina', 'anna karenina unabridged']], ['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', ' ', []], From 8ac0ce399f8dbe5082fb933c2510423b1251ab8a Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 30 Nov 2023 21:17:13 +0200 Subject: [PATCH 0205/2145] Remove "et al[.]" in author cleanup --- server/finders/BookFinder.js | 2 ++ test/server/finders/BookFinder.test.js | 1 + 2 files changed, 3 insertions(+) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 0c5f32d2..4422fa98 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -462,6 +462,8 @@ function cleanAuthorForCompares(author) { 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, '') + // remove et al. + cleanAuthor = cleanAuthor.replace(/et al\.?/g, '') return cleanAuthor } diff --git a/test/server/finders/BookFinder.test.js b/test/server/finders/BookFinder.test.js index ed2442c6..5d28bbea 100644 --- a/test/server/finders/BookFinder.test.js +++ b/test/server/finders/BookFinder.test.js @@ -111,6 +111,7 @@ describe('AuthorCandidates', () => { ['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 (et al removed)', 'nikolai gogol et al.', ['nikolai gogol']], ['adds normalized recognized candidate (normalized initials)', 'j.k. rowling', ['j. k. rowling']], ].forEach(([name, author, expected]) => it(name, async () => { authorCandidates.add(author) From 281de48ed4b0bc67d48e7a8ec1f85e4dbdeaa247 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 30 Nov 2023 21:49:24 +0200 Subject: [PATCH 0206/2145] Fix "et al" cleanup --- server/finders/BookFinder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 4422fa98..b76b8b1d 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -463,7 +463,7 @@ function cleanAuthorForCompares(author) { // remove middle initials cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '') // remove et al. - cleanAuthor = cleanAuthor.replace(/et al\.?/g, '') + cleanAuthor = cleanAuthor.replace(/ et al\.?(?= |$)/g, '') return cleanAuthor } 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 0207/2145] 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 e29239a0..547f5b05 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 0208/2145] 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 c582c264..4bb68727 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 0209/2145] 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 4a40f2b7..5806f105 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 41a28b4d..00000000 --- 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 0210/2145] 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 5806f105..075c0fd4 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 711c526a..a16e6fa1 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 267db5c8..26a9d77b 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 26578f57..ebad97db 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 0211/2145] 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 57792177..267bbb45 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 0212/2145] 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 4fa8e394..2009b28d 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 e645569e..ffb1feb7 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 78e64804..eb3d59f4 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 857627e9..02f9df05 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 267bbb45..c20d532a 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 26a9d77b..e209fac9 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 bf3db557..6e9d8456 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 0213/2145] 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 c20d532a..b5bc7d40 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 0214/2145] 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 b5bc7d40..0a282c9c 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 b1b325d00b3595b6b9b77d5fbc5e4259a18ec1ba Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Tue, 5 Dec 2023 21:18:30 +0200 Subject: [PATCH 0215/2145] Add ffbinaries dependency --- package-lock.json | 1133 +++++++++++++++++++++++++++++++++++++++++++-- package.json | 1 + 2 files changed, 1095 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9df54fdd..6880ebdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "cookie-parser": "^1.4.6", "express": "^4.17.1", "express-session": "^1.17.3", + "ffbinaries": "^1.1.5", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "lru-cache": "^10.0.3", @@ -887,6 +888,21 @@ "node": ">=8" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -981,6 +997,22 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -990,11 +1022,29 @@ "node": "*" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + }, "node_modules/axios": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", @@ -1017,6 +1067,14 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1108,11 +1166,24 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, "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/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/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1218,6 +1289,11 @@ } ] }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, "node_modules/chai": { "version": "4.3.10", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", @@ -1320,6 +1396,11 @@ "node": ">=10" } }, + "node_modules/clarg": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/clarg/-/clarg-0.0.4.tgz", + "integrity": "sha512-SZ3fE0m3MpngjwCyuHNIPgNZ+2EOCEzHtDRP/Y+zlRdP1mQntNKeTjRdtYouxaqV9Lx/BVbrZXIXgOnRwyosDg==" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -1402,6 +1483,52 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -1465,6 +1592,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1491,6 +1623,38 @@ "node": ">= 8" } }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1641,6 +1805,15 @@ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1894,6 +2067,78 @@ "node": ">= 0.6" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "dependencies": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + } + }, + "node_modules/extract-zip/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "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==" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/ffbinaries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ffbinaries/-/ffbinaries-1.1.5.tgz", + "integrity": "sha512-41RwpEb6tC1fmiyaJtHLn8OHmxG9XjH8/ZtxSFxkXoYmR+4Xk4LeSMzBC9ZA9T6hj60UoLEv1I0mL8zq3uTAsA==", + "dependencies": { + "async": "^3.1.0", + "clarg": "0.0.4", + "extract-zip": "^1.6.7", + "fs-extra": "^8.1.0", + "lodash": "^4.17.15", + "request": "^2.88.0" + }, + "bin": { + "ffbinaries": "cli.js" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1994,6 +2239,14 @@ "node": ">=8.0.0" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -2043,6 +2296,19 @@ } ] }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -2135,6 +2401,14 @@ "node": ">=8.0.0" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2180,6 +2454,27 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2323,6 +2618,20 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -2522,8 +2831,7 @@ "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 + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "node_modules/is-unicode-supported": { "version": "0.1.0", @@ -2552,11 +2860,10 @@ "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 + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -2786,6 +3093,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -2798,6 +3110,21 @@ "node": ">=4" } }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2810,6 +3137,14 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -2861,6 +3196,20 @@ "node": ">=10" } }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", @@ -3106,6 +3455,14 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", @@ -3620,6 +3977,12 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "optional": true + }, "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", @@ -3677,6 +4040,21 @@ "node": ">=10" } }, + "node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -3848,6 +4226,14 @@ "node": ">=8" } }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4100,6 +4486,16 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, "node_modules/pg-connection-string": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", @@ -4135,6 +4531,11 @@ "node": ">=8" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -4178,12 +4579,25 @@ "node": ">= 0.10" } }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, "node_modules/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 }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -4274,6 +4688,67 @@ "node": ">=4" } }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4819,6 +5294,27 @@ "node": ">=8" } }, + "node_modules/spawn-wrap/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/spawn-wrap/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4847,6 +5343,30 @@ } } }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ssrf-req-filter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz", @@ -5032,11 +5552,39 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -5067,6 +5615,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -5111,6 +5664,14 @@ "imurmurhash": "^0.1.4" } }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5149,6 +5710,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5186,6 +5755,19 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -5200,21 +5782,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", @@ -5417,6 +5984,15 @@ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6113,6 +6689,17 @@ "indent-string": "^4.0.0" } }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -6186,17 +6773,45 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" + }, "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 }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" + }, + "aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + }, "axios": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", @@ -6216,6 +6831,14 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "requires": { + "tweetnacl": "^0.14.3" + } + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -6277,11 +6900,21 @@ "update-browserslist-db": "^1.0.13" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" + }, "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==" }, + "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==" + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -6357,6 +6990,11 @@ "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", "dev": true }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, "chai": { "version": "4.3.10", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", @@ -6429,6 +7067,11 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" }, + "clarg": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/clarg/-/clarg-0.0.4.tgz", + "integrity": "sha512-SZ3fE0m3MpngjwCyuHNIPgNZ+2EOCEzHtDRP/Y+zlRdP1mQntNKeTjRdtYouxaqV9Lx/BVbrZXIXgOnRwyosDg==" + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -6498,6 +7141,51 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -6548,6 +7236,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, "cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -6566,6 +7259,31 @@ "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "requires": { + "assert-plus": "^1.0.0" } }, "debug": { @@ -6669,6 +7387,15 @@ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -6871,6 +7598,68 @@ } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "requires": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "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==" + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "requires": { + "pend": "~1.2.0" + } + }, + "ffbinaries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ffbinaries/-/ffbinaries-1.1.5.tgz", + "integrity": "sha512-41RwpEb6tC1fmiyaJtHLn8OHmxG9XjH8/ZtxSFxkXoYmR+4Xk4LeSMzBC9ZA9T6hj60UoLEv1I0mL8zq3uTAsA==", + "requires": { + "async": "^3.1.0", + "clarg": "0.0.4", + "extract-zip": "^1.6.7", + "fs-extra": "^8.1.0", + "lodash": "^4.17.15", + "request": "^2.88.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6936,6 +7725,11 @@ "signal-exit": "^3.0.2" } }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" + }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -6962,6 +7756,16 @@ "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", "dev": true }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -7030,6 +7834,14 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "requires": { + "assert-plus": "^1.0.0" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -7063,6 +7875,20 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -7166,6 +7992,16 @@ } } }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -7317,8 +8153,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "is-unicode-supported": { "version": "0.1.0", @@ -7338,11 +8173,10 @@ "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 + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "istanbul-lib-coverage": { "version": "3.2.2", @@ -7518,18 +8352,46 @@ "esprima": "^4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "requires": { + "graceful-fs": "^4.1.6" + } + }, "jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -7570,6 +8432,17 @@ } } }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, "just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", @@ -7771,6 +8644,11 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, "minipass": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", @@ -8155,6 +9033,12 @@ "wide-align": "^1.1.5" } }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "optional": true + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -8193,6 +9077,15 @@ "requires": { "lru-cache": "^6.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" + } } } }, @@ -8334,6 +9227,11 @@ } } }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8514,6 +9412,16 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, "pg-connection-string": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", @@ -8540,6 +9448,11 @@ "find-up": "^4.0.0" } }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -8574,12 +9487,22 @@ "ipaddr.js": "1.9.1" } }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, "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 }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, "qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -8646,6 +9569,55 @@ "es6-error": "^4.0.1" } }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9032,6 +10004,23 @@ "rimraf": "^3.0.0", "signal-exit": "^3.0.2", "which": "^2.0.1" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, "sprintf-js": { @@ -9051,6 +10040,22 @@ "tar": "^6.1.11" } }, + "sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, "ssrf-req-filter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz", @@ -9192,11 +10197,33 @@ "nopt": "~1.0.10" } }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -9218,6 +10245,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -9259,6 +10291,11 @@ "imurmurhash": "^0.1.4" } }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -9274,6 +10311,14 @@ "picocolors": "^1.0.0" } }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9299,6 +10344,16 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -9313,15 +10368,6 @@ "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==", - "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", @@ -9480,6 +10526,15 @@ } } }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -9487,4 +10542,4 @@ "dev": true } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 061e2a7f..1bb95075 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "cookie-parser": "^1.4.6", "express": "^4.17.1", "express-session": "^1.17.3", + "ffbinaries": "^1.1.5", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "lru-cache": "^10.0.3", From 2e989fbe83f00691ed0a955fe82d0e1bf1b1b7df Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Tue, 5 Dec 2023 21:19:17 +0200 Subject: [PATCH 0216/2145] Add BinaryManager --- .gitignore | 2 + server/Server.js | 3 + server/managers/BinaryManager.js | 79 +++++++ test/server/managers/BinaryManager.test.js | 262 +++++++++++++++++++++ 4 files changed, 346 insertions(+) create mode 100644 server/managers/BinaryManager.js create mode 100644 test/server/managers/BinaryManager.test.js diff --git a/.gitignore b/.gitignore index 9360600a..0690f38f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ /deploy/ /coverage/ /.nyc_output/ +/ffmpeg* +/ffprobe* sw.* .DS_STORE diff --git a/server/Server.js b/server/Server.js index 5e8cab76..9104208d 100644 --- a/server/Server.js +++ b/server/Server.js @@ -33,6 +33,7 @@ const AudioMetadataMangaer = require('./managers/AudioMetadataManager') const RssFeedManager = require('./managers/RssFeedManager') const CronManager = require('./managers/CronManager') const ApiCacheManager = require('./managers/ApiCacheManager') +const BinaryManager = require('./managers/BinaryManager') const LibraryScanner = require('./scanner/LibraryScanner') //Import the main Passport and Express-Session library @@ -74,6 +75,7 @@ class Server { this.rssFeedManager = new RssFeedManager() this.cronManager = new CronManager(this.podcastManager) this.apiCacheManager = new ApiCacheManager() + this.binaryManager = new BinaryManager() // Routers this.apiRouter = new ApiRouter(this) @@ -119,6 +121,7 @@ class Server { const libraries = await Database.libraryModel.getAllOldLibraries() await this.cronManager.init(libraries) this.apiCacheManager.init() + await this.binaryManager.init() if (Database.serverSettings.scannerDisableWatcher) { Logger.info(`[Server] Watcher is disabled`) diff --git a/server/managers/BinaryManager.js b/server/managers/BinaryManager.js new file mode 100644 index 00000000..d2a3c1f7 --- /dev/null +++ b/server/managers/BinaryManager.js @@ -0,0 +1,79 @@ +const path = require('path') +const which = require('../libs/which') +const fs = require('../libs/fsExtra') +const Logger = require('../Logger') +const ffbinaries = require('ffbinaries') +const { promisify } = require('util') + +class BinaryManager { + downloadBinaries = promisify(ffbinaries.downloadBinaries) + + defaultRequiredBinaries = [ + { name: 'ffmpeg', envVariable: 'FFMPEG_PATH' }, + { name: 'ffprobe', envVariable: 'FFPROBE_PATH' } + ] + + constructor(requiredBinaries = this.defaultRequiredBinaries) { + this.requiredBinaries = requiredBinaries + this.mainInstallPath = process.pkg ? path.dirname(process.execPath) : global.appRoot + this.altInstallPath = global.ConfigPath + } + + async init() { + if (this.initialized) return + const missingBinaries = await this.findRequiredBinaries() + if (missingBinaries.length == 0) return + await this.install(missingBinaries) + const missingBinariesAfterInstall = await this.findRequiredBinaries() + if (missingBinariesAfterInstall.length != 0) { + Logger.error(`[BinaryManager] Failed to find or install required binaries: ${missingBinariesAfterInstall.join(', ')}`) + process.exit(1) + } + this.initialized = true + } + + async findRequiredBinaries() { + const missingBinaries = [] + for (const binary of this.requiredBinaries) { + const binaryPath = await this.findBinary(binary.name, binary.envVariable) + if (binaryPath) { + Logger.info(`[BinaryManager] Found ${binary.name} at ${binaryPath}`) + Logger.info(`[BinaryManager] Updating process.env.${binary.envVariable}`) + process.env[binary.envVariable] = binaryPath + } else { + Logger.info(`[BinaryManager] ${binary.name} not found`) + missingBinaries.push(binary.name) + } + } + return missingBinaries + } + + async findBinary(name, envVariable) { + const executable = name + (process.platform == 'win32' ? '.exe' : '') + const defaultPath = process.env[envVariable] + if (defaultPath && await fs.pathExists(defaultPath)) return defaultPath + const whichPath = which.sync(executable, { nothrow: true }) + if (whichPath) return whichPath + const mainInstallPath = path.join(this.mainInstallPath, executable) + if (await fs.pathExists(mainInstallPath)) return mainInstallPath + const altInstallPath = path.join(this.altInstallPath, executable) + if (await fs.pathExists(altInstallPath)) return altInstallPath + return null + } + + async install(binaries) { + if (binaries.length == 0) return + Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`) + let destination = this.mainInstallPath + try { + await fs.access(destination, fs.constants.W_OK) + } catch (err) { + destination = this.altInstallPath + } + await this.downloadBinaries(binaries, { destination }) + Logger.info(`[BinaryManager] Binaries installed to ${destination}`) + } + +} + +module.exports = BinaryManager \ No newline at end of file diff --git a/test/server/managers/BinaryManager.test.js b/test/server/managers/BinaryManager.test.js new file mode 100644 index 00000000..8e09f62f --- /dev/null +++ b/test/server/managers/BinaryManager.test.js @@ -0,0 +1,262 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const fs = require('../../../server/libs/fsExtra'); +const which = require('../../../server/libs/which'); +const path = require('path'); +const BinaryManager = require('../../../server/managers/BinaryManager'); + +const expect = chai.expect; + +describe('BinaryManager', () => { + let binaryManager; + + describe('init', () => { + let findStub; + let installStub; + let errorStub; + let exitStub; + + beforeEach(() => { + binaryManager = new BinaryManager(); + findStub = sinon.stub(binaryManager, 'findRequiredBinaries'); + installStub = sinon.stub(binaryManager, 'install'); + errorStub = sinon.stub(console, 'error'); + exitStub = sinon.stub(process, 'exit'); + }); + + afterEach(() => { + findStub.restore(); + installStub.restore(); + errorStub.restore(); + exitStub.restore(); + }); + + it('should not install binaries if they are already found', async () => { + findStub.resolves([]); + + await binaryManager.init(); + + expect(installStub.called).to.be.false; + expect(findStub.calledOnce).to.be.true; + expect(errorStub.called).to.be.false; + expect(exitStub.called).to.be.false; + }); + + it('should install missing binaries', async () => { + const missingBinaries = ['ffmpeg', 'ffprobe']; + const missingBinariesAfterInstall = []; + findStub.onFirstCall().resolves(missingBinaries); + findStub.onSecondCall().resolves(missingBinariesAfterInstall); + + await binaryManager.init(); + + expect(findStub.calledTwice).to.be.true; + expect(installStub.calledOnce).to.be.true; + expect(errorStub.called).to.be.false; + expect(exitStub.called).to.be.false; + }); + + it('exit if binaries are not found after installation', async () => { + const missingBinaries = ['ffmpeg', 'ffprobe']; + const missingBinariesAfterInstall = ['ffmpeg', 'ffprobe']; + findStub.onFirstCall().resolves(missingBinaries); + findStub.onSecondCall().resolves(missingBinariesAfterInstall); + + await binaryManager.init(); + + expect(findStub.calledTwice).to.be.true; + expect(installStub.calledOnce).to.be.true; + expect(errorStub.calledOnce).to.be.true; + expect(exitStub.calledOnce).to.be.true; + expect(exitStub.calledWith(1)).to.be.true; + }); + }); + + + describe('findRequiredBinaries', () => { + let findBinaryStub; + + beforeEach(() => { + const requiredBinaries = [{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH' }]; + binaryManager = new BinaryManager(requiredBinaries); + findBinaryStub = sinon.stub(binaryManager, 'findBinary'); + }); + + afterEach(() => { + findBinaryStub.restore(); + }); + + it('should put found paths in the correct environment variables', async () => { + const pathToFFmpeg = '/path/to/ffmpeg'; + const missingBinaries = []; + delete process.env.FFMPEG_PATH; + findBinaryStub.resolves(pathToFFmpeg); + + const result = await binaryManager.findRequiredBinaries(); + + expect(result).to.deep.equal(missingBinaries); + expect(findBinaryStub.calledOnce).to.be.true; + expect(process.env.FFMPEG_PATH).to.equal(pathToFFmpeg); + }); + + it('should add missing binaries to result', async () => { + const missingBinaries = ['ffmpeg']; + delete process.env.FFMPEG_PATH; + findBinaryStub.resolves(null); + + const result = await binaryManager.findRequiredBinaries(); + + expect(result).to.deep.equal(missingBinaries); + expect(findBinaryStub.calledOnce).to.be.true; + expect(process.env.FFMPEG_PATH).to.be.undefined; + }); + }); + + describe('install', () => { + let accessStub; + let downloadBinariesStub; + + beforeEach(() => { + binaryManager = new BinaryManager(); + accessStub = sinon.stub(fs, 'access'); + downloadBinariesStub = sinon.stub(binaryManager, 'downloadBinaries'); + binaryManager.mainInstallPath = '/path/to/main/install' + binaryManager.altInstallPath = '/path/to/alt/install' + }); + + afterEach(() => { + accessStub.restore(); + downloadBinariesStub.restore(); + }); + + it('should not install binaries if no binaries are passed', async () => { + const binaries = []; + + await binaryManager.install(binaries); + + expect(accessStub.called).to.be.false; + expect(downloadBinariesStub.called).to.be.false; + }); + + it('should install binaries in main install path if has access', async () => { + const binaries = ['ffmpeg']; + const destination = binaryManager.mainInstallPath; + accessStub.withArgs(destination, fs.constants.W_OK).resolves(); + downloadBinariesStub.resolves(); + + await binaryManager.install(binaries); + + expect(accessStub.calledOnce).to.be.true; + expect(downloadBinariesStub.calledOnce).to.be.true; + expect(downloadBinariesStub.calledWith(binaries, sinon.match({ destination: destination }))).to.be.true; + }); + + it('should install binaries in alt install path if has no access to main', async () => { + const binaries = ['ffmpeg']; + const mainDestination = binaryManager.mainInstallPath; + const destination = binaryManager.altInstallPath; + accessStub.withArgs(mainDestination, fs.constants.W_OK).rejects(); + downloadBinariesStub.resolves(); + + await binaryManager.install(binaries); + + expect(accessStub.calledOnce).to.be.true; + expect(downloadBinariesStub.calledOnce).to.be.true; + expect(downloadBinariesStub.calledWith(binaries, sinon.match({ destination: destination }))).to.be.true; + }); + }); +}); + +describe('findBinary', () => { + let binaryManager; + let fsPathExistsStub; + let whichSyncStub; + let mainInstallPath; + let altInstallPath; + + const name = 'ffmpeg'; + const envVariable = 'FFMPEG_PATH'; + const defaultPath = '/path/to/ffmpeg'; + const executable = name + (process.platform == 'win32' ? '.exe' : ''); + const whichPath = '/usr/bin/ffmpeg'; + + + beforeEach(() => { + binaryManager = new BinaryManager(); + fsPathExistsStub = sinon.stub(fs, 'pathExists'); + whichSyncStub = sinon.stub(which, 'sync'); + binaryManager.mainInstallPath = '/path/to/main/install' + mainInstallPath = path.join(binaryManager.mainInstallPath, executable); + binaryManager.altInstallPath = '/path/to/alt/install' + altInstallPath = path.join(binaryManager.altInstallPath, executable); + }); + + afterEach(() => { + fsPathExistsStub.restore(); + whichSyncStub.restore(); + }); + + it('should return defaultPath if it exists', async () => { + process.env[envVariable] = defaultPath; + fsPathExistsStub.withArgs(defaultPath).resolves(true); + + const result = await binaryManager.findBinary(name, envVariable); + + expect(result).to.equal(defaultPath); + expect(fsPathExistsStub.calledOnceWith(defaultPath)).to.be.true; + expect(whichSyncStub.notCalled).to.be.true; + }); + + it('should return whichPath if it exists', async () => { + delete process.env[envVariable]; + whichSyncStub.returns(whichPath); + + const result = await binaryManager.findBinary(name, envVariable); + + expect(result).to.equal(whichPath); + expect(fsPathExistsStub.notCalled).to.be.true; + expect(whichSyncStub.calledOnce).to.be.true; + }); + + it('should return mainInstallPath if it exists', async () => { + delete process.env[envVariable]; + whichSyncStub.returns(null); + fsPathExistsStub.withArgs(mainInstallPath).resolves(true); + + const result = await binaryManager.findBinary(name, envVariable); + + expect(result).to.equal(mainInstallPath); + expect(whichSyncStub.calledOnce).to.be.true; + expect(fsPathExistsStub.calledOnceWith(mainInstallPath)).to.be.true; + }); + + it('should return altInstallPath if it exists', async () => { + delete process.env[envVariable]; + whichSyncStub.returns(null); + fsPathExistsStub.withArgs(mainInstallPath).resolves(false); + fsPathExistsStub.withArgs(altInstallPath).resolves(true); + + const result = await binaryManager.findBinary(name, envVariable); + + expect(result).to.equal(altInstallPath); + expect(whichSyncStub.calledOnce).to.be.true; + expect(fsPathExistsStub.calledTwice).to.be.true; + expect(fsPathExistsStub.calledWith(mainInstallPath)).to.be.true; + expect(fsPathExistsStub.calledWith(altInstallPath)).to.be.true; + }); + + it('should return null if binary is not found', async () => { + delete process.env[envVariable]; + whichSyncStub.returns(null); + fsPathExistsStub.withArgs(mainInstallPath).resolves(false); + fsPathExistsStub.withArgs(altInstallPath).resolves(false); + + const result = await binaryManager.findBinary(name, envVariable); + + expect(result).to.be.null; + expect(whichSyncStub.calledOnce).to.be.true; + expect(fsPathExistsStub.calledTwice).to.be.true; + expect(fsPathExistsStub.calledWith(mainInstallPath)).to.be.true; + expect(fsPathExistsStub.calledWith(altInstallPath)).to.be.true; + }); +}); \ No newline at end of file From c074c835d4c9eeefc008ce95aa4141164a072f5d Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Tue, 5 Dec 2023 22:18:37 +0200 Subject: [PATCH 0217/2145] Remove semicolons from test --- test/server/managers/BinaryManager.test.js | 352 ++++++++++----------- 1 file changed, 176 insertions(+), 176 deletions(-) diff --git a/test/server/managers/BinaryManager.test.js b/test/server/managers/BinaryManager.test.js index 8e09f62f..26b14721 100644 --- a/test/server/managers/BinaryManager.test.js +++ b/test/server/managers/BinaryManager.test.js @@ -1,262 +1,262 @@ -const chai = require('chai'); -const sinon = require('sinon'); -const fs = require('../../../server/libs/fsExtra'); -const which = require('../../../server/libs/which'); -const path = require('path'); -const BinaryManager = require('../../../server/managers/BinaryManager'); +const chai = require('chai') +const sinon = require('sinon') +const fs = require('../../../server/libs/fsExtra') +const which = require('../../../server/libs/which') +const path = require('path') +const BinaryManager = require('../../../server/managers/BinaryManager') -const expect = chai.expect; +const expect = chai.expect describe('BinaryManager', () => { - let binaryManager; + let binaryManager describe('init', () => { - let findStub; - let installStub; - let errorStub; - let exitStub; + let findStub + let installStub + let errorStub + let exitStub beforeEach(() => { - binaryManager = new BinaryManager(); - findStub = sinon.stub(binaryManager, 'findRequiredBinaries'); - installStub = sinon.stub(binaryManager, 'install'); - errorStub = sinon.stub(console, 'error'); - exitStub = sinon.stub(process, 'exit'); - }); + binaryManager = new BinaryManager() + findStub = sinon.stub(binaryManager, 'findRequiredBinaries') + installStub = sinon.stub(binaryManager, 'install') + errorStub = sinon.stub(console, 'error') + exitStub = sinon.stub(process, 'exit') + }) afterEach(() => { - findStub.restore(); - installStub.restore(); - errorStub.restore(); - exitStub.restore(); - }); + findStub.restore() + installStub.restore() + errorStub.restore() + exitStub.restore() + }) it('should not install binaries if they are already found', async () => { - findStub.resolves([]); + findStub.resolves([]) - await binaryManager.init(); + await binaryManager.init() - expect(installStub.called).to.be.false; - expect(findStub.calledOnce).to.be.true; - expect(errorStub.called).to.be.false; - expect(exitStub.called).to.be.false; - }); + expect(installStub.called).to.be.false + expect(findStub.calledOnce).to.be.true + expect(errorStub.called).to.be.false + expect(exitStub.called).to.be.false + }) it('should install missing binaries', async () => { - const missingBinaries = ['ffmpeg', 'ffprobe']; - const missingBinariesAfterInstall = []; - findStub.onFirstCall().resolves(missingBinaries); - findStub.onSecondCall().resolves(missingBinariesAfterInstall); + const missingBinaries = ['ffmpeg', 'ffprobe'] + const missingBinariesAfterInstall = [] + findStub.onFirstCall().resolves(missingBinaries) + findStub.onSecondCall().resolves(missingBinariesAfterInstall) - await binaryManager.init(); + await binaryManager.init() - expect(findStub.calledTwice).to.be.true; - expect(installStub.calledOnce).to.be.true; - expect(errorStub.called).to.be.false; - expect(exitStub.called).to.be.false; - }); + expect(findStub.calledTwice).to.be.true + expect(installStub.calledOnce).to.be.true + expect(errorStub.called).to.be.false + expect(exitStub.called).to.be.false + }) it('exit if binaries are not found after installation', async () => { - const missingBinaries = ['ffmpeg', 'ffprobe']; - const missingBinariesAfterInstall = ['ffmpeg', 'ffprobe']; - findStub.onFirstCall().resolves(missingBinaries); - findStub.onSecondCall().resolves(missingBinariesAfterInstall); + const missingBinaries = ['ffmpeg', 'ffprobe'] + const missingBinariesAfterInstall = ['ffmpeg', 'ffprobe'] + findStub.onFirstCall().resolves(missingBinaries) + findStub.onSecondCall().resolves(missingBinariesAfterInstall) - await binaryManager.init(); + await binaryManager.init() - expect(findStub.calledTwice).to.be.true; - expect(installStub.calledOnce).to.be.true; - expect(errorStub.calledOnce).to.be.true; - expect(exitStub.calledOnce).to.be.true; - expect(exitStub.calledWith(1)).to.be.true; - }); - }); + expect(findStub.calledTwice).to.be.true + expect(installStub.calledOnce).to.be.true + expect(errorStub.calledOnce).to.be.true + expect(exitStub.calledOnce).to.be.true + expect(exitStub.calledWith(1)).to.be.true + }) + }) describe('findRequiredBinaries', () => { - let findBinaryStub; + let findBinaryStub beforeEach(() => { - const requiredBinaries = [{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH' }]; - binaryManager = new BinaryManager(requiredBinaries); - findBinaryStub = sinon.stub(binaryManager, 'findBinary'); - }); + const requiredBinaries = [{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH' }] + binaryManager = new BinaryManager(requiredBinaries) + findBinaryStub = sinon.stub(binaryManager, 'findBinary') + }) afterEach(() => { - findBinaryStub.restore(); - }); + findBinaryStub.restore() + }) it('should put found paths in the correct environment variables', async () => { - const pathToFFmpeg = '/path/to/ffmpeg'; - const missingBinaries = []; - delete process.env.FFMPEG_PATH; - findBinaryStub.resolves(pathToFFmpeg); + const pathToFFmpeg = '/path/to/ffmpeg' + const missingBinaries = [] + delete process.env.FFMPEG_PATH + findBinaryStub.resolves(pathToFFmpeg) - const result = await binaryManager.findRequiredBinaries(); + const result = await binaryManager.findRequiredBinaries() - expect(result).to.deep.equal(missingBinaries); - expect(findBinaryStub.calledOnce).to.be.true; - expect(process.env.FFMPEG_PATH).to.equal(pathToFFmpeg); - }); + expect(result).to.deep.equal(missingBinaries) + expect(findBinaryStub.calledOnce).to.be.true + expect(process.env.FFMPEG_PATH).to.equal(pathToFFmpeg) + }) it('should add missing binaries to result', async () => { - const missingBinaries = ['ffmpeg']; - delete process.env.FFMPEG_PATH; - findBinaryStub.resolves(null); + const missingBinaries = ['ffmpeg'] + delete process.env.FFMPEG_PATH + findBinaryStub.resolves(null) - const result = await binaryManager.findRequiredBinaries(); + const result = await binaryManager.findRequiredBinaries() - expect(result).to.deep.equal(missingBinaries); - expect(findBinaryStub.calledOnce).to.be.true; - expect(process.env.FFMPEG_PATH).to.be.undefined; - }); - }); + expect(result).to.deep.equal(missingBinaries) + expect(findBinaryStub.calledOnce).to.be.true + expect(process.env.FFMPEG_PATH).to.be.undefined + }) + }) describe('install', () => { - let accessStub; - let downloadBinariesStub; + let accessStub + let downloadBinariesStub beforeEach(() => { - binaryManager = new BinaryManager(); - accessStub = sinon.stub(fs, 'access'); - downloadBinariesStub = sinon.stub(binaryManager, 'downloadBinaries'); + binaryManager = new BinaryManager() + accessStub = sinon.stub(fs, 'access') + downloadBinariesStub = sinon.stub(binaryManager, 'downloadBinaries') binaryManager.mainInstallPath = '/path/to/main/install' binaryManager.altInstallPath = '/path/to/alt/install' - }); + }) afterEach(() => { - accessStub.restore(); - downloadBinariesStub.restore(); - }); + accessStub.restore() + downloadBinariesStub.restore() + }) it('should not install binaries if no binaries are passed', async () => { - const binaries = []; + const binaries = [] - await binaryManager.install(binaries); + await binaryManager.install(binaries) - expect(accessStub.called).to.be.false; - expect(downloadBinariesStub.called).to.be.false; - }); + expect(accessStub.called).to.be.false + expect(downloadBinariesStub.called).to.be.false + }) it('should install binaries in main install path if has access', async () => { - const binaries = ['ffmpeg']; - const destination = binaryManager.mainInstallPath; - accessStub.withArgs(destination, fs.constants.W_OK).resolves(); - downloadBinariesStub.resolves(); + const binaries = ['ffmpeg'] + const destination = binaryManager.mainInstallPath + accessStub.withArgs(destination, fs.constants.W_OK).resolves() + downloadBinariesStub.resolves() - await binaryManager.install(binaries); + await binaryManager.install(binaries) - expect(accessStub.calledOnce).to.be.true; - expect(downloadBinariesStub.calledOnce).to.be.true; - expect(downloadBinariesStub.calledWith(binaries, sinon.match({ destination: destination }))).to.be.true; - }); + expect(accessStub.calledOnce).to.be.true + expect(downloadBinariesStub.calledOnce).to.be.true + expect(downloadBinariesStub.calledWith(binaries, sinon.match({ destination: destination }))).to.be.true + }) it('should install binaries in alt install path if has no access to main', async () => { - const binaries = ['ffmpeg']; - const mainDestination = binaryManager.mainInstallPath; - const destination = binaryManager.altInstallPath; - accessStub.withArgs(mainDestination, fs.constants.W_OK).rejects(); - downloadBinariesStub.resolves(); + const binaries = ['ffmpeg'] + const mainDestination = binaryManager.mainInstallPath + const destination = binaryManager.altInstallPath + accessStub.withArgs(mainDestination, fs.constants.W_OK).rejects() + downloadBinariesStub.resolves() - await binaryManager.install(binaries); + await binaryManager.install(binaries) - expect(accessStub.calledOnce).to.be.true; - expect(downloadBinariesStub.calledOnce).to.be.true; - expect(downloadBinariesStub.calledWith(binaries, sinon.match({ destination: destination }))).to.be.true; - }); - }); -}); + expect(accessStub.calledOnce).to.be.true + expect(downloadBinariesStub.calledOnce).to.be.true + expect(downloadBinariesStub.calledWith(binaries, sinon.match({ destination: destination }))).to.be.true + }) + }) +}) describe('findBinary', () => { - let binaryManager; - let fsPathExistsStub; - let whichSyncStub; - let mainInstallPath; - let altInstallPath; + let binaryManager + let fsPathExistsStub + let whichSyncStub + let mainInstallPath + let altInstallPath - const name = 'ffmpeg'; - const envVariable = 'FFMPEG_PATH'; - const defaultPath = '/path/to/ffmpeg'; - const executable = name + (process.platform == 'win32' ? '.exe' : ''); - const whichPath = '/usr/bin/ffmpeg'; + const name = 'ffmpeg' + const envVariable = 'FFMPEG_PATH' + const defaultPath = '/path/to/ffmpeg' + const executable = name + (process.platform == 'win32' ? '.exe' : '') + const whichPath = '/usr/bin/ffmpeg' beforeEach(() => { - binaryManager = new BinaryManager(); - fsPathExistsStub = sinon.stub(fs, 'pathExists'); - whichSyncStub = sinon.stub(which, 'sync'); + binaryManager = new BinaryManager() + fsPathExistsStub = sinon.stub(fs, 'pathExists') + whichSyncStub = sinon.stub(which, 'sync') binaryManager.mainInstallPath = '/path/to/main/install' - mainInstallPath = path.join(binaryManager.mainInstallPath, executable); + mainInstallPath = path.join(binaryManager.mainInstallPath, executable) binaryManager.altInstallPath = '/path/to/alt/install' - altInstallPath = path.join(binaryManager.altInstallPath, executable); - }); + altInstallPath = path.join(binaryManager.altInstallPath, executable) + }) afterEach(() => { - fsPathExistsStub.restore(); - whichSyncStub.restore(); - }); + fsPathExistsStub.restore() + whichSyncStub.restore() + }) it('should return defaultPath if it exists', async () => { - process.env[envVariable] = defaultPath; - fsPathExistsStub.withArgs(defaultPath).resolves(true); + process.env[envVariable] = defaultPath + fsPathExistsStub.withArgs(defaultPath).resolves(true) - const result = await binaryManager.findBinary(name, envVariable); + const result = await binaryManager.findBinary(name, envVariable) - expect(result).to.equal(defaultPath); - expect(fsPathExistsStub.calledOnceWith(defaultPath)).to.be.true; - expect(whichSyncStub.notCalled).to.be.true; - }); + expect(result).to.equal(defaultPath) + expect(fsPathExistsStub.calledOnceWith(defaultPath)).to.be.true + expect(whichSyncStub.notCalled).to.be.true + }) it('should return whichPath if it exists', async () => { - delete process.env[envVariable]; - whichSyncStub.returns(whichPath); + delete process.env[envVariable] + whichSyncStub.returns(whichPath) - const result = await binaryManager.findBinary(name, envVariable); + const result = await binaryManager.findBinary(name, envVariable) - expect(result).to.equal(whichPath); - expect(fsPathExistsStub.notCalled).to.be.true; - expect(whichSyncStub.calledOnce).to.be.true; - }); + expect(result).to.equal(whichPath) + expect(fsPathExistsStub.notCalled).to.be.true + expect(whichSyncStub.calledOnce).to.be.true + }) it('should return mainInstallPath if it exists', async () => { - delete process.env[envVariable]; - whichSyncStub.returns(null); - fsPathExistsStub.withArgs(mainInstallPath).resolves(true); + delete process.env[envVariable] + whichSyncStub.returns(null) + fsPathExistsStub.withArgs(mainInstallPath).resolves(true) - const result = await binaryManager.findBinary(name, envVariable); + const result = await binaryManager.findBinary(name, envVariable) - expect(result).to.equal(mainInstallPath); - expect(whichSyncStub.calledOnce).to.be.true; - expect(fsPathExistsStub.calledOnceWith(mainInstallPath)).to.be.true; - }); + expect(result).to.equal(mainInstallPath) + expect(whichSyncStub.calledOnce).to.be.true + expect(fsPathExistsStub.calledOnceWith(mainInstallPath)).to.be.true + }) it('should return altInstallPath if it exists', async () => { - delete process.env[envVariable]; - whichSyncStub.returns(null); - fsPathExistsStub.withArgs(mainInstallPath).resolves(false); - fsPathExistsStub.withArgs(altInstallPath).resolves(true); + delete process.env[envVariable] + whichSyncStub.returns(null) + fsPathExistsStub.withArgs(mainInstallPath).resolves(false) + fsPathExistsStub.withArgs(altInstallPath).resolves(true) - const result = await binaryManager.findBinary(name, envVariable); + const result = await binaryManager.findBinary(name, envVariable) - expect(result).to.equal(altInstallPath); - expect(whichSyncStub.calledOnce).to.be.true; - expect(fsPathExistsStub.calledTwice).to.be.true; - expect(fsPathExistsStub.calledWith(mainInstallPath)).to.be.true; - expect(fsPathExistsStub.calledWith(altInstallPath)).to.be.true; - }); + expect(result).to.equal(altInstallPath) + expect(whichSyncStub.calledOnce).to.be.true + expect(fsPathExistsStub.calledTwice).to.be.true + expect(fsPathExistsStub.calledWith(mainInstallPath)).to.be.true + expect(fsPathExistsStub.calledWith(altInstallPath)).to.be.true + }) it('should return null if binary is not found', async () => { - delete process.env[envVariable]; - whichSyncStub.returns(null); - fsPathExistsStub.withArgs(mainInstallPath).resolves(false); - fsPathExistsStub.withArgs(altInstallPath).resolves(false); + delete process.env[envVariable] + whichSyncStub.returns(null) + fsPathExistsStub.withArgs(mainInstallPath).resolves(false) + fsPathExistsStub.withArgs(altInstallPath).resolves(false) - const result = await binaryManager.findBinary(name, envVariable); + const result = await binaryManager.findBinary(name, envVariable) - expect(result).to.be.null; - expect(whichSyncStub.calledOnce).to.be.true; - expect(fsPathExistsStub.calledTwice).to.be.true; - expect(fsPathExistsStub.calledWith(mainInstallPath)).to.be.true; - expect(fsPathExistsStub.calledWith(altInstallPath)).to.be.true; - }); -}); \ No newline at end of file + expect(result).to.be.null + expect(whichSyncStub.calledOnce).to.be.true + expect(fsPathExistsStub.calledTwice).to.be.true + expect(fsPathExistsStub.calledWith(mainInstallPath)).to.be.true + expect(fsPathExistsStub.calledWith(altInstallPath)).to.be.true + }) +}) \ No newline at end of file From 1ce1904c892e79f6de8a901d72fe3ad5746a3a3e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 5 Dec 2023 17:35:15 -0600 Subject: [PATCH 0218/2145] Add ffbinaries lib --- server/libs/ffbinaries/index.js | 354 +++++++++++++++++++++++++++++++ server/managers/BinaryManager.js | 18 +- 2 files changed, 362 insertions(+), 10 deletions(-) create mode 100644 server/libs/ffbinaries/index.js diff --git a/server/libs/ffbinaries/index.js b/server/libs/ffbinaries/index.js new file mode 100644 index 00000000..4794fd85 --- /dev/null +++ b/server/libs/ffbinaries/index.js @@ -0,0 +1,354 @@ +const os = require('os') +const path = require('path') +const axios = require('axios') +const fse = require('../fsExtra') +const async = require('../async') +const StreamZip = require('../nodeStreamZip') + +var API_URL = 'https://ffbinaries.com/api/v1' + +var LOCAL_CACHE_DIR = path.join(os.homedir() + '/.ffbinaries-cache') +var RUNTIME_CACHE = {} +var errorMsgs = { + connectionIssues: 'Couldn\'t connect to ffbinaries.com API. Check your Internet connection.', + parsingVersionData: 'Couldn\'t parse retrieved version data. Try "ffbinaries clearcache".', + parsingVersionList: 'Couldn\'t parse the list of available versions. Try "ffbinaries clearcache".', + notFound: 'Requested data not found.', + incorrectVersionParam: '"version" parameter must be a string.' +} + +function ensureDirSync(dir) { + try { + fse.accessSync(dir) + } catch (e) { + fse.mkdirSync(dir) + } +} + +ensureDirSync(LOCAL_CACHE_DIR) + +/** + * Resolves the platform key based on input string + */ +function resolvePlatform(input) { + var rtn = null + + switch (input) { + case 'mac': + case 'osx': + case 'mac-64': + case 'osx-64': + rtn = 'osx-64' + break + + case 'linux': + case 'linux-32': + rtn = 'linux-32' + break + + case 'linux-64': + rtn = 'linux-64' + break + + case 'linux-arm': + case 'linux-armel': + rtn = 'linux-armel' + break + + case 'linux-armhf': + rtn = 'linux-armhf' + break + + case 'win': + case 'win-32': + case 'windows': + case 'windows-32': + rtn = 'windows-32' + break + + case 'win-64': + case 'windows-64': + rtn = 'windows-64' + break + + default: + rtn = null + } + + return rtn +} +/** + * Detects the platform of the machine the script is executed on. + * Object can be provided to detect platform from info derived elsewhere. + * + * @param {object} osinfo Contains "type" and "arch" properties + */ +function detectPlatform(osinfo) { + var inputIsValid = typeof osinfo === 'object' && typeof osinfo.type === 'string' && typeof osinfo.arch === 'string' + var type = (inputIsValid ? osinfo.type : os.type()).toLowerCase() + var arch = (inputIsValid ? osinfo.arch : os.arch()).toLowerCase() + + if (type === 'darwin') { + return 'osx-64' + } + + if (type === 'windows_nt') { + return arch === 'x64' ? 'windows-64' : 'windows-32' + } + + if (type === 'linux') { + if (arch === 'arm' || arch === 'arm64') { + return 'linux-armel' + } + return arch === 'x64' ? 'linux-64' : 'linux-32' + } + + return null +} +/** + * Gets the binary filename (appends exe in Windows) + * + * @param {string} component "ffmpeg", "ffplay", "ffprobe" or "ffserver" + * @param {platform} platform "ffmpeg", "ffplay", "ffprobe" or "ffserver" + */ +function getBinaryFilename(component, platform) { + var platformCode = resolvePlatform(platform) + if (platformCode === 'windows-32' || platformCode === 'windows-64') { + return component + '.exe' + } + return component +} + +function listPlatforms() { + return ['osx-64', 'linux-32', 'linux-64', 'linux-armel', 'linux-armhf', 'windows-32', 'windows-64'] +} + +/** + * + * @returns {Promise<string[]>} array of version strings + */ +function listVersions() { + if (RUNTIME_CACHE.versionsAll) { + return RUNTIME_CACHE.versionsAll + } + return axios.get(API_URL).then((res) => { + if (!res.data?.versions || !Object.keys(res.data.versions)?.length) { + throw new Error(errorMsgs.parsingVersionList) + } + const versionKeys = Object.keys(res.data.versions) + RUNTIME_CACHE.versionsAll = versionKeys + return versionKeys + }) +} +/** + * Gets full data set from ffbinaries.com + */ +function getVersionData(version) { + if (RUNTIME_CACHE[version]) { + return RUNTIME_CACHE[version] + } + + if (version && typeof version !== 'string') { + throw new Error(errorMsgs.incorrectVersionParam) + } + + var url = version ? '/version/' + version : '/latest' + + return axios.get(`${API_URL}${url}`).then((res) => { + RUNTIME_CACHE[version] = res.data + return res.data + }).catch((error) => { + if (error.response?.status == 404) { + throw new Error(errorMsgs.notFound) + } else { + throw new Error(errorMsgs.connectionIssues) + } + }) +} + +/** + * Download file(s) and save them in the specified directory + */ +function downloadUrls(components, urls, opts, callback) { + var destinationDir = opts.destination + var results = [] + const remappedUrls = [] + + if (components && !Array.isArray(components)) { + components = [components] + } else if (!components || !Array.isArray(components)) { + components = [] + } + + // returns an array of objects like this: {component: 'ffmpeg', url: 'https://...'} + if (typeof urls === 'object') { + for (const key in urls) { + if (components.includes(key) && urls[key]) { + remappedUrls.push({ + component: key, + url: urls[key] + }) + } + } + } + + + async function extractZipToDestination(zipFilename, cb) { + var oldpath = path.join(LOCAL_CACHE_DIR, zipFilename) + const zip = new StreamZip.async({ file: oldpath }) + const count = await zip.extract(null, destinationDir) + console.log(`Extracted ${count} entries`) + await zip.close() + cb() + } + + + async.each(remappedUrls, function (urlObject, cb) { + if (!urlObject?.url || !urlObject?.component) { + return cb() + } + + var url = urlObject.url + + var zipFilename = url.split('/').pop() + var binFilenameBase = urlObject.component + var binFilename = getBinaryFilename(binFilenameBase, opts.platform || detectPlatform()) + var runningTotal = 0 + var totalFilesize + var interval + + if (typeof opts.tickerFn === 'function') { + opts.tickerInterval = parseInt(opts.tickerInterval, 10) + var tickerInterval = (!Number.isNaN(opts.tickerInterval)) ? opts.tickerInterval : 1000 + var tickData = { filename: zipFilename, progress: 0 } + + // Schedule next ticks + interval = setInterval(function () { + if (totalFilesize && runningTotal == totalFilesize) { + return clearInterval(interval) + } + tickData.progress = totalFilesize > -1 ? runningTotal / totalFilesize : 0 + + opts.tickerFn(tickData) + }, tickerInterval) + } + + try { + if (opts.force) { + throw new Error('Force mode specified - will overwrite existing binaries in target location') + } + + // Check if file already exists in target directory + var binPath = path.join(destinationDir, binFilename) + fse.accessSync(binPath) + // if the accessSync method doesn't throw we know the binary already exists + results.push({ + filename: binFilename, + path: destinationDir, + status: 'File exists', + code: 'FILE_EXISTS' + }) + clearInterval(interval) + return cb() + } catch (errBinExists) { + var zipPath = path.join(LOCAL_CACHE_DIR, zipFilename) + + // If there's no binary then check if the zip file is already in cache + try { + fse.accessSync(zipPath) + results.push({ + filename: binFilename, + path: destinationDir, + status: 'File extracted to destination (archive found in cache)', + code: 'DONE_FROM_CACHE' + }) + clearInterval(interval) + return extractZipToDestination(zipFilename, cb) + } catch (errZipExists) { + // If zip is not cached then download it and store in cache + if (opts.quiet) clearInterval(interval) + + var cacheFileTempName = zipPath + '.part' + var cacheFileFinalName = zipPath + + axios({ + url, + method: 'GET', + responseType: 'stream' + }).then((response) => { + totalFilesize = response.headers?.['content-length'] || [] + + // Write to filepath + const writer = fse.createWriteStream(cacheFileTempName) + response.data.pipe(writer) + + writer.on('finish', () => { + results.push({ + filename: binFilename, + path: destinationDir, + size: Math.floor(totalFilesize / 1024 / 1024 * 1000) / 1000 + 'MB', + status: 'File extracted to destination (downloaded from "' + url + '")', + code: 'DONE_CLEAN' + }) + + fse.renameSync(cacheFileTempName, cacheFileFinalName) + extractZipToDestination(zipFilename, cb) + }) + writer.on('error', (err) => { + // TODO: Handle writer err + throw new Error(err) + }) + }).catch((err) => { + // TODO: Handle error + console.error(`Failed to download file "${zipFilename}"`, err) + cb() + }) + } + } + }, function () { + return callback(null, results) + }) +} + +/** + * Gets binaries for the platform + * It will get the data from ffbinaries, pick the correct files + * and save it to the specified directory + * + * @param {Array} components + * @param {Object} [opts] + */ +async function downloadBinaries(components, opts = {}) { + var platform = resolvePlatform(opts.platform) || detectPlatform() + + opts.destination = path.resolve(opts.destination || '.') + ensureDirSync(opts.destination) + + const versionData = await getVersionData(opts.version) + const urls = versionData?.bin?.[platform] + if (!urls) { + throw new Error('No URLs!') + } + + return new Promise((resolve, reject) => { + downloadUrls(components, urls, opts, (err, data) => { + if (err) reject(err) + else resolve(data) + }) + }) +} + +function clearCache() { + fse.emptyDirSync(LOCAL_CACHE_DIR) +} + +module.exports = { + downloadBinaries: downloadBinaries, + getVersionData: getVersionData, + listVersions: listVersions, + listPlatforms: listPlatforms, + detectPlatform: detectPlatform, + resolvePlatform: resolvePlatform, + getBinaryFilename: getBinaryFilename, + clearCache: clearCache +} \ No newline at end of file diff --git a/server/managers/BinaryManager.js b/server/managers/BinaryManager.js index d2a3c1f7..771bb7e9 100644 --- a/server/managers/BinaryManager.js +++ b/server/managers/BinaryManager.js @@ -1,16 +1,14 @@ const path = require('path') const which = require('../libs/which') const fs = require('../libs/fsExtra') +const ffbinaries = require('../libs/ffbinaries') const Logger = require('../Logger') -const ffbinaries = require('ffbinaries') -const { promisify } = require('util') -class BinaryManager { - downloadBinaries = promisify(ffbinaries.downloadBinaries) +class BinaryManager { - defaultRequiredBinaries = [ - { name: 'ffmpeg', envVariable: 'FFMPEG_PATH' }, - { name: 'ffprobe', envVariable: 'FFPROBE_PATH' } + defaultRequiredBinaries = [ + { name: 'ffmpeg', envVariable: 'FFMPEG_PATH' }, + { name: 'ffprobe', envVariable: 'FFPROBE_PATH' } ] constructor(requiredBinaries = this.defaultRequiredBinaries) { @@ -65,12 +63,12 @@ class BinaryManager { if (binaries.length == 0) return Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`) let destination = this.mainInstallPath - try { + try { await fs.access(destination, fs.constants.W_OK) - } catch (err) { + } catch (err) { destination = this.altInstallPath } - await this.downloadBinaries(binaries, { destination }) + await ffbinaries.downloadBinaries(binaries, { destination }) Logger.info(`[BinaryManager] Binaries installed to ${destination}`) } From 61a0126278853d4661ed7418cd0233274a78e31c Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 5 Dec 2023 17:35:57 -0600 Subject: [PATCH 0219/2145] Remove ffbinaries dependency --- package-lock.json | 987 +--------------------------------------------- package.json | 3 +- 2 files changed, 5 insertions(+), 985 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6880ebdf..1ef4b091 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "cookie-parser": "^1.4.6", "express": "^4.17.1", "express-session": "^1.17.3", - "ffbinaries": "^1.1.5", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "lru-cache": "^10.0.3", @@ -888,21 +887,6 @@ "node": ">=8" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -997,22 +981,6 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -1022,29 +990,11 @@ "node": "*" } }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" - }, "node_modules/axios": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", @@ -1067,14 +1017,6 @@ "node": "^4.5.0 || >= 5.9" } }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1166,24 +1108,11 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "engines": { - "node": "*" - } - }, "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/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/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1289,11 +1218,6 @@ } ] }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, "node_modules/chai": { "version": "4.3.10", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", @@ -1396,11 +1320,6 @@ "node": ">=10" } }, - "node_modules/clarg": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/clarg/-/clarg-0.0.4.tgz", - "integrity": "sha512-SZ3fE0m3MpngjwCyuHNIPgNZ+2EOCEzHtDRP/Y+zlRdP1mQntNKeTjRdtYouxaqV9Lx/BVbrZXIXgOnRwyosDg==" - }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -1483,52 +1402,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/concat-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/concat-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -1592,11 +1465,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1644,17 +1512,6 @@ "node": ">= 8" } }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1805,15 +1662,6 @@ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2067,78 +1915,6 @@ "node": ">= 0.6" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", - "dependencies": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - } - }, - "node_modules/extract-zip/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "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==" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/ffbinaries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ffbinaries/-/ffbinaries-1.1.5.tgz", - "integrity": "sha512-41RwpEb6tC1fmiyaJtHLn8OHmxG9XjH8/ZtxSFxkXoYmR+4Xk4LeSMzBC9ZA9T6hj60UoLEv1I0mL8zq3uTAsA==", - "dependencies": { - "async": "^3.1.0", - "clarg": "0.0.4", - "extract-zip": "^1.6.7", - "fs-extra": "^8.1.0", - "lodash": "^4.17.15", - "request": "^2.88.0" - }, - "bin": { - "ffbinaries": "cli.js" - } - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2239,14 +2015,6 @@ "node": ">=8.0.0" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "engines": { - "node": "*" - } - }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -2296,19 +2064,6 @@ } ] }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -2401,14 +2156,6 @@ "node": ">=8.0.0" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2454,27 +2201,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2618,20 +2344,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -2831,7 +2543,8 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true }, "node_modules/is-unicode-supported": { "version": "0.1.0", @@ -2860,11 +2573,6 @@ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "dev": true }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -3093,11 +2801,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -3110,21 +2813,6 @@ "node": ">=4" } }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3137,14 +2825,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -3196,20 +2876,6 @@ "node": ">=10" } }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", @@ -3455,14 +3121,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", @@ -4226,14 +3884,6 @@ "node": ">=8" } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4486,16 +4136,6 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, "node_modules/pg-connection-string": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", @@ -4531,11 +4171,6 @@ "node": ">=8" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, "node_modules/process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -4579,25 +4214,12 @@ "node": ">= 0.10" } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "node_modules/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 }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -4688,67 +4310,6 @@ "node": ">=4" } }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/request/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5343,30 +4904,6 @@ } } }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ssrf-req-filter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz", @@ -5552,39 +5089,11 @@ "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -5615,11 +5124,6 @@ "node": ">= 0.6" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -5664,14 +5168,6 @@ "imurmurhash": "^0.1.4" } }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5710,14 +5206,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5755,19 +5243,6 @@ "node": ">= 0.8" } }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -5984,15 +5459,6 @@ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6689,17 +6155,6 @@ "indent-string": "^4.0.0" } }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -6773,45 +6228,17 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, - "asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" - }, "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 }, - "async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" - }, - "aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" - }, "axios": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", @@ -6831,14 +6258,6 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "requires": { - "tweetnacl": "^0.14.3" - } - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -6900,21 +6319,11 @@ "update-browserslist-db": "^1.0.13" } }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" - }, "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==" }, - "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==" - }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -6990,11 +6399,6 @@ "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", "dev": true }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, "chai": { "version": "4.3.10", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", @@ -7067,11 +6471,6 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" }, - "clarg": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/clarg/-/clarg-0.0.4.tgz", - "integrity": "sha512-SZ3fE0m3MpngjwCyuHNIPgNZ+2EOCEzHtDRP/Y+zlRdP1mQntNKeTjRdtYouxaqV9Lx/BVbrZXIXgOnRwyosDg==" - }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -7141,51 +6540,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -7236,11 +6590,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - }, "cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -7278,14 +6627,6 @@ } } }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "requires": { - "assert-plus": "^1.0.0" - } - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7387,15 +6728,6 @@ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -7598,68 +6930,6 @@ } } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", - "requires": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", - "yauzl": "^2.10.0" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "requires": { - "minimist": "^1.2.6" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "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==" - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "requires": { - "pend": "~1.2.0" - } - }, - "ffbinaries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ffbinaries/-/ffbinaries-1.1.5.tgz", - "integrity": "sha512-41RwpEb6tC1fmiyaJtHLn8OHmxG9XjH8/ZtxSFxkXoYmR+4Xk4LeSMzBC9ZA9T6hj60UoLEv1I0mL8zq3uTAsA==", - "requires": { - "async": "^3.1.0", - "clarg": "0.0.4", - "extract-zip": "^1.6.7", - "fs-extra": "^8.1.0", - "lodash": "^4.17.15", - "request": "^2.88.0" - } - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -7725,11 +6995,6 @@ "signal-exit": "^3.0.2" } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" - }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -7756,16 +7021,6 @@ "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", "dev": true }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -7834,14 +7089,6 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "requires": { - "assert-plus": "^1.0.0" - } - }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -7875,20 +7122,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -7992,16 +7225,6 @@ } } }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8153,7 +7376,8 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true }, "is-unicode-supported": { "version": "0.1.0", @@ -8173,11 +7397,6 @@ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "dev": true }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, "istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -8352,46 +7571,18 @@ "esprima": "^4.0.0" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, - "json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "requires": { - "graceful-fs": "^4.1.6" - } - }, "jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -8432,17 +7623,6 @@ } } }, - "jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, "just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", @@ -8644,11 +7824,6 @@ "brace-expansion": "^1.1.7" } }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - }, "minipass": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", @@ -9227,11 +8402,6 @@ } } }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9412,16 +8582,6 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, "pg-connection-string": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", @@ -9448,11 +8608,6 @@ "find-up": "^4.0.0" } }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, "process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -9487,22 +8642,12 @@ "ipaddr.js": "1.9.1" } }, - "psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "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 }, - "punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" - }, "qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -9569,55 +8714,6 @@ "es6-error": "^4.0.1" } }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - } - } - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -10040,22 +9136,6 @@ "tar": "^6.1.11" } }, - "sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, "ssrf-req-filter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz", @@ -10197,33 +9277,11 @@ "nopt": "~1.0.10" } }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -10245,11 +9303,6 @@ "mime-types": "~2.1.24" } }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -10291,11 +9344,6 @@ "imurmurhash": "^0.1.4" } }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -10311,14 +9359,6 @@ "picocolors": "^1.0.0" } }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10344,16 +9384,6 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -10526,15 +9556,6 @@ } } }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "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 1bb95075..1e0c559d 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "cookie-parser": "^1.4.6", "express": "^4.17.1", "express-session": "^1.17.3", - "ffbinaries": "^1.1.5", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "lru-cache": "^10.0.3", @@ -62,4 +61,4 @@ "nyc": "^15.1.0", "sinon": "^17.0.1" } -} \ No newline at end of file +} 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 0220/2145] 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 c6579ab3..1af069f3 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 67ccd2c1fb9a9ebc8378d11de09bd9d7a6477578 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 6 Dec 2023 13:45:28 +0200 Subject: [PATCH 0221/2145] Fix test after switching to libs/ffbinaries --- test/server/managers/BinaryManager.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/server/managers/BinaryManager.test.js b/test/server/managers/BinaryManager.test.js index 26b14721..f9cc4df6 100644 --- a/test/server/managers/BinaryManager.test.js +++ b/test/server/managers/BinaryManager.test.js @@ -2,6 +2,7 @@ const chai = require('chai') const sinon = require('sinon') const fs = require('../../../server/libs/fsExtra') const which = require('../../../server/libs/which') +const ffbinaries = require('../../../server/libs/ffbinaries') const path = require('path') const BinaryManager = require('../../../server/managers/BinaryManager') @@ -119,7 +120,7 @@ describe('BinaryManager', () => { beforeEach(() => { binaryManager = new BinaryManager() accessStub = sinon.stub(fs, 'access') - downloadBinariesStub = sinon.stub(binaryManager, 'downloadBinaries') + downloadBinariesStub = sinon.stub(ffbinaries, 'downloadBinaries') binaryManager.mainInstallPath = '/path/to/main/install' binaryManager.altInstallPath = '/path/to/alt/install' }) 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 0222/2145] 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 31719eef..e46ed323 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 699a658df930d3d750f022965c542ec19c0b4221 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 7 Dec 2023 08:50:45 +0200 Subject: [PATCH 0223/2145] Remove debug printing from libs/ffbinaries --- server/libs/ffbinaries/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/libs/ffbinaries/index.js b/server/libs/ffbinaries/index.js index 4794fd85..f5cc9a1c 100644 --- a/server/libs/ffbinaries/index.js +++ b/server/libs/ffbinaries/index.js @@ -197,7 +197,6 @@ function downloadUrls(components, urls, opts, callback) { var oldpath = path.join(LOCAL_CACHE_DIR, zipFilename) const zip = new StreamZip.async({ file: oldpath }) const count = await zip.extract(null, destinationDir) - console.log(`Extracted ${count} entries`) await zip.close() cb() } From 18b3ab561009774e5e6f135af2bb4ab508a3a5ef Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 7 Dec 2023 15:12:49 -0600 Subject: [PATCH 0224/2145] Revert package-lock updates --- package-lock.json | 150 ++++++++++++---------------------------------- package.json | 2 +- 2 files changed, 38 insertions(+), 114 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ef4b091..9df54fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1491,27 +1491,6 @@ "node": ">= 8" } }, - "node_modules/cross-spawn/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2573,6 +2552,12 @@ "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 + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -3635,12 +3620,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true - }, "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", @@ -3698,21 +3677,6 @@ "node": ">=10" } }, - "node_modules/node-gyp/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "optional": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -4855,27 +4819,6 @@ "node": ">=8" } }, - "node_modules/spawn-wrap/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/spawn-wrap/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -5257,6 +5200,21 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", @@ -6608,23 +6566,6 @@ "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" - }, - "dependencies": { - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } } }, "debug": { @@ -7397,6 +7338,12 @@ "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 + }, "istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -8208,12 +8155,6 @@ "wide-align": "^1.1.5" } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -8252,15 +8193,6 @@ "requires": { "lru-cache": "^6.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" - } } } }, @@ -9100,23 +9032,6 @@ "rimraf": "^3.0.0", "signal-exit": "^3.0.2", "which": "^2.0.1" - }, - "dependencies": { - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } } }, "sprintf-js": { @@ -9398,6 +9313,15 @@ "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==", + "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", @@ -9563,4 +9487,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 1e0c559d..061e2a7f 100644 --- a/package.json +++ b/package.json @@ -61,4 +61,4 @@ "nyc": "^15.1.0", "sinon": "^17.0.1" } -} +} \ No newline at end of file From 09282a9a62380c5e58be58a19eb24535798a0c01 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 7 Dec 2023 23:49:46 +0200 Subject: [PATCH 0225/2145] Remove all callbacks and refactor spaghetti code in downloadUrls --- server/libs/ffbinaries/index.js | 186 +++++++++++++++----------------- 1 file changed, 85 insertions(+), 101 deletions(-) diff --git a/server/libs/ffbinaries/index.js b/server/libs/ffbinaries/index.js index f5cc9a1c..c09c4237 100644 --- a/server/libs/ffbinaries/index.js +++ b/server/libs/ffbinaries/index.js @@ -4,6 +4,7 @@ const axios = require('axios') const fse = require('../fsExtra') const async = require('../async') const StreamZip = require('../nodeStreamZip') +const { finished } = require('stream/promises') var API_URL = 'https://ffbinaries.com/api/v1' @@ -169,9 +170,9 @@ function getVersionData(version) { /** * Download file(s) and save them in the specified directory */ -function downloadUrls(components, urls, opts, callback) { - var destinationDir = opts.destination - var results = [] +async function downloadUrls(components, urls, opts) { + const destinationDir = opts.destination + const results = [] const remappedUrls = [] if (components && !Array.isArray(components)) { @@ -193,68 +194,61 @@ function downloadUrls(components, urls, opts, callback) { } - async function extractZipToDestination(zipFilename, cb) { - var oldpath = path.join(LOCAL_CACHE_DIR, zipFilename) + async function extractZipToDestination(zipFilename) { + const oldpath = path.join(LOCAL_CACHE_DIR, zipFilename) const zip = new StreamZip.async({ file: oldpath }) const count = await zip.extract(null, destinationDir) await zip.close() - cb() } - async.each(remappedUrls, function (urlObject, cb) { - if (!urlObject?.url || !urlObject?.component) { - return cb() - } - - var url = urlObject.url - - var zipFilename = url.split('/').pop() - var binFilenameBase = urlObject.component - var binFilename = getBinaryFilename(binFilenameBase, opts.platform || detectPlatform()) - var runningTotal = 0 - var totalFilesize - var interval - - if (typeof opts.tickerFn === 'function') { - opts.tickerInterval = parseInt(opts.tickerInterval, 10) - var tickerInterval = (!Number.isNaN(opts.tickerInterval)) ? opts.tickerInterval : 1000 - var tickData = { filename: zipFilename, progress: 0 } - - // Schedule next ticks - interval = setInterval(function () { - if (totalFilesize && runningTotal == totalFilesize) { - return clearInterval(interval) - } - tickData.progress = totalFilesize > -1 ? runningTotal / totalFilesize : 0 - - opts.tickerFn(tickData) - }, tickerInterval) - } - + await async.each(remappedUrls, async function (urlObject) { try { - if (opts.force) { - throw new Error('Force mode specified - will overwrite existing binaries in target location') + const url = urlObject.url + + const zipFilename = url.split('/').pop() + const binFilenameBase = urlObject.component + const binFilename = getBinaryFilename(binFilenameBase, opts.platform || detectPlatform()) + + let runningTotal = 0 + let totalFilesize + let interval + + + if (typeof opts.tickerFn === 'function') { + opts.tickerInterval = parseInt(opts.tickerInterval, 10) + const tickerInterval = (!Number.isNaN(opts.tickerInterval)) ? opts.tickerInterval : 1000 + const tickData = { filename: zipFilename, progress: 0 } + + // Schedule next ticks + interval = setInterval(function () { + if (totalFilesize && runningTotal == totalFilesize) { + return clearInterval(interval) + } + tickData.progress = totalFilesize > -1 ? runningTotal / totalFilesize : 0 + + opts.tickerFn(tickData) + }, tickerInterval) } + // Check if file already exists in target directory - var binPath = path.join(destinationDir, binFilename) - fse.accessSync(binPath) - // if the accessSync method doesn't throw we know the binary already exists - results.push({ - filename: binFilename, - path: destinationDir, - status: 'File exists', - code: 'FILE_EXISTS' - }) - clearInterval(interval) - return cb() - } catch (errBinExists) { - var zipPath = path.join(LOCAL_CACHE_DIR, zipFilename) + const binPath = path.join(destinationDir, binFilename) + if (!opts.force && await fse.pathExists(binPath)) { + // if the accessSync method doesn't throw we know the binary already exists + results.push({ + filename: binFilename, + path: destinationDir, + status: 'File exists', + code: 'FILE_EXISTS' + }) + clearInterval(interval) + return + } // If there's no binary then check if the zip file is already in cache - try { - fse.accessSync(zipPath) + const zipPath = path.join(LOCAL_CACHE_DIR, zipFilename) + if (await fse.pathExists(zipPath)) { results.push({ filename: binFilename, path: destinationDir, @@ -262,51 +256,46 @@ function downloadUrls(components, urls, opts, callback) { code: 'DONE_FROM_CACHE' }) clearInterval(interval) - return extractZipToDestination(zipFilename, cb) - } catch (errZipExists) { - // If zip is not cached then download it and store in cache - if (opts.quiet) clearInterval(interval) - - var cacheFileTempName = zipPath + '.part' - var cacheFileFinalName = zipPath - - axios({ - url, - method: 'GET', - responseType: 'stream' - }).then((response) => { - totalFilesize = response.headers?.['content-length'] || [] - - // Write to filepath - const writer = fse.createWriteStream(cacheFileTempName) - response.data.pipe(writer) - - writer.on('finish', () => { - results.push({ - filename: binFilename, - path: destinationDir, - size: Math.floor(totalFilesize / 1024 / 1024 * 1000) / 1000 + 'MB', - status: 'File extracted to destination (downloaded from "' + url + '")', - code: 'DONE_CLEAN' - }) - - fse.renameSync(cacheFileTempName, cacheFileFinalName) - extractZipToDestination(zipFilename, cb) - }) - writer.on('error', (err) => { - // TODO: Handle writer err - throw new Error(err) - }) - }).catch((err) => { - // TODO: Handle error - console.error(`Failed to download file "${zipFilename}"`, err) - cb() - }) + await extractZipToDestination(zipFilename) + return } + + // If zip is not cached then download it and store in cache + if (opts.quiet) clearInterval(interval) + + const cacheFileTempName = zipPath + '.part' + const cacheFileFinalName = zipPath + + const response = await axios({ + url, + method: 'GET', + responseType: 'stream' + }) + totalFilesize = response.headers?.['content-length'] || [] + + // Write to cacheFileTempName + const writer = fse.createWriteStream(cacheFileTempName) + response.data.on('data', (chunk) => { + runningTotal += chunk.length + }) + response.data.pipe(writer) + await finished(writer) + await fse.rename(cacheFileTempName, cacheFileFinalName) + await extractZipToDestination(zipFilename) + + results.push({ + filename: binFilename, + path: destinationDir, + size: Math.floor(totalFilesize / 1024 / 1024 * 1000) / 1000 + 'MB', + status: 'File extracted to destination (downloaded from "' + url + '")', + code: 'DONE_CLEAN' + }) + } catch (err) { + console.error(`Failed to download or extract file for component: ${urlObject.component}`, err) } - }, function () { - return callback(null, results) }) + + return results } /** @@ -329,12 +318,7 @@ async function downloadBinaries(components, opts = {}) { throw new Error('No URLs!') } - return new Promise((resolve, reject) => { - downloadUrls(components, urls, opts, (err, data) => { - if (err) reject(err) - else resolve(data) - }) - }) + return await downloadUrls(components, urls, opts) } function clearCache() { From 6afb8de3dd962aa16fc1b743b9c7b879c16de67b Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Fri, 8 Dec 2023 00:53:53 +0200 Subject: [PATCH 0226/2145] Remove ffbinaries local cache --- server/libs/ffbinaries/index.js | 42 ++++++++------------------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/server/libs/ffbinaries/index.js b/server/libs/ffbinaries/index.js index c09c4237..b0660f08 100644 --- a/server/libs/ffbinaries/index.js +++ b/server/libs/ffbinaries/index.js @@ -8,12 +8,11 @@ const { finished } = require('stream/promises') var API_URL = 'https://ffbinaries.com/api/v1' -var LOCAL_CACHE_DIR = path.join(os.homedir() + '/.ffbinaries-cache') var RUNTIME_CACHE = {} var errorMsgs = { connectionIssues: 'Couldn\'t connect to ffbinaries.com API. Check your Internet connection.', - parsingVersionData: 'Couldn\'t parse retrieved version data. Try "ffbinaries clearcache".', - parsingVersionList: 'Couldn\'t parse the list of available versions. Try "ffbinaries clearcache".', + parsingVersionData: 'Couldn\'t parse retrieved version data.', + parsingVersionList: 'Couldn\'t parse the list of available versions.', notFound: 'Requested data not found.', incorrectVersionParam: '"version" parameter must be a string.' } @@ -26,8 +25,6 @@ function ensureDirSync(dir) { } } -ensureDirSync(LOCAL_CACHE_DIR) - /** * Resolves the platform key based on input string */ @@ -195,7 +192,7 @@ async function downloadUrls(components, urls, opts) { async function extractZipToDestination(zipFilename) { - const oldpath = path.join(LOCAL_CACHE_DIR, zipFilename) + const oldpath = path.join(destinationDir, zipFilename) const zip = new StreamZip.async({ file: oldpath }) const count = await zip.extract(null, destinationDir) await zip.close() @@ -246,25 +243,11 @@ async function downloadUrls(components, urls, opts) { return } - // If there's no binary then check if the zip file is already in cache - const zipPath = path.join(LOCAL_CACHE_DIR, zipFilename) - if (await fse.pathExists(zipPath)) { - results.push({ - filename: binFilename, - path: destinationDir, - status: 'File extracted to destination (archive found in cache)', - code: 'DONE_FROM_CACHE' - }) - clearInterval(interval) - await extractZipToDestination(zipFilename) - return - } - - // If zip is not cached then download it and store in cache if (opts.quiet) clearInterval(interval) - const cacheFileTempName = zipPath + '.part' - const cacheFileFinalName = zipPath + const zipPath = path.join(destinationDir, zipFilename) + const zipFileTempName = zipPath + '.part' + const zipFileFinalName = zipPath const response = await axios({ url, @@ -273,15 +256,15 @@ async function downloadUrls(components, urls, opts) { }) totalFilesize = response.headers?.['content-length'] || [] - // Write to cacheFileTempName - const writer = fse.createWriteStream(cacheFileTempName) + const writer = fse.createWriteStream(zipFileTempName) response.data.on('data', (chunk) => { runningTotal += chunk.length }) response.data.pipe(writer) await finished(writer) - await fse.rename(cacheFileTempName, cacheFileFinalName) + await fse.rename(zipFileTempName, zipFileFinalName) await extractZipToDestination(zipFilename) + await fse.remove(zipFileFinalName) results.push({ filename: binFilename, @@ -321,10 +304,6 @@ async function downloadBinaries(components, opts = {}) { return await downloadUrls(components, urls, opts) } -function clearCache() { - fse.emptyDirSync(LOCAL_CACHE_DIR) -} - module.exports = { downloadBinaries: downloadBinaries, getVersionData: getVersionData, @@ -332,6 +311,5 @@ module.exports = { listPlatforms: listPlatforms, detectPlatform: detectPlatform, resolvePlatform: resolvePlatform, - getBinaryFilename: getBinaryFilename, - clearCache: clearCache + getBinaryFilename: getBinaryFilename } \ No newline at end of file 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 0227/2145] 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 ffb1feb7..9e028307 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 e209fac9..db4110e0 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 0228/2145] 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 b8936024..6d39569e 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 a93507c0..fa28dd24 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 fc2f0316..47315301 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 f10a51f4..f6efa428 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 d65bb13e..0317e2f9 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 b172c2e5..eb4f074f 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 50f384e7..eb7d27d8 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 638e3468..7e526721 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 3e3fda41..9c4b9a63 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 08845488..d4779abd 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 8cbfd919..511c8b86 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 bf34cbac..b51084e9 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 b0ba0f6a..b48e0dbd 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 6883af39..fde0cd87 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 14bfcc0b..7c559489 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 6f6395bad7d8981405b5e59014a22749da7b2734 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 7 Dec 2023 17:32:06 -0600 Subject: [PATCH 0229/2145] Only log update binary env path if it was updated --- server/managers/BinaryManager.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/managers/BinaryManager.js b/server/managers/BinaryManager.js index 771bb7e9..98cb79b2 100644 --- a/server/managers/BinaryManager.js +++ b/server/managers/BinaryManager.js @@ -36,8 +36,10 @@ class BinaryManager { const binaryPath = await this.findBinary(binary.name, binary.envVariable) if (binaryPath) { Logger.info(`[BinaryManager] Found ${binary.name} at ${binaryPath}`) - Logger.info(`[BinaryManager] Updating process.env.${binary.envVariable}`) - process.env[binary.envVariable] = binaryPath + if (process.env[binary.envVariable] !== binaryPath) { + Logger.info(`[BinaryManager] Updating process.env.${binary.envVariable}`) + process.env[binary.envVariable] = binaryPath + } } else { Logger.info(`[BinaryManager] ${binary.name} not found`) missingBinaries.push(binary.name) 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 0230/2145] 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 da8af1d8..3fa18960 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", From 0282a0521b8466c0af521f017c5a16dd8fcdfa8a Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sat, 9 Dec 2023 00:33:06 +0200 Subject: [PATCH 0231/2145] Sort audible match results by duration difference --- client/components/modals/item/tabs/Match.vue | 1 + server/controllers/SearchController.js | 5 +- server/finders/BookFinder.js | 26 ++++++++-- server/scanner/Scanner.js | 2 +- test/server/finders/BookFinder.test.js | 54 ++++++++++++++++---- 5 files changed, 70 insertions(+), 18 deletions(-) diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 1c682919..b57e9612 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -332,6 +332,7 @@ export default { if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}` var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}` if (this.searchAuthor) searchQuery += `&author=${encodeURIComponent(this.searchAuthor)}` + if (this.libraryItemId) searchQuery += `&id=${this.libraryItemId}` return searchQuery }, submitSearch() { diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index 93587bc4..e52e6973 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -3,15 +3,18 @@ const BookFinder = require('../finders/BookFinder') const PodcastFinder = require('../finders/PodcastFinder') const AuthorFinder = require('../finders/AuthorFinder') const MusicFinder = require('../finders/MusicFinder') +const Database = require("../Database") class SearchController { constructor() { } async findBooks(req, res) { + const id = req.query.id + const libraryItem = await Database.libraryItemModel.getOldById(id) const provider = req.query.provider || 'google' const title = req.query.title || '' const author = req.query.author || '' - const results = await BookFinder.search(provider, title, author) + const results = await BookFinder.search(libraryItem, provider, title, author) res.json(results) } diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index b76b8b1d..8704a964 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -299,6 +299,7 @@ class BookFinder { /** * Search for books including fuzzy searches * + * @param {Object} libraryItem * @param {string} provider * @param {string} title * @param {string} author @@ -307,7 +308,7 @@ class BookFinder { * @param {{titleDistance:number, authorDistance:number, maxFuzzySearches:number}} options * @returns {Promise<Object[]>} */ - async search(provider, title, author, isbn, asin, options = {}) { + async search(libraryItem, provider, title, author, isbn, asin, options = {}) { let books = [] const maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4 const maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4 @@ -336,6 +337,7 @@ class BookFinder { for (const titlePart of titleParts) authorCandidates.add(titlePart) authorCandidates = await authorCandidates.getCandidates() + loop_author: for (const authorCandidate of authorCandidates) { let titleCandidates = new BookFinder.TitleCandidates(authorCandidate) for (const titlePart of titleParts) @@ -343,13 +345,27 @@ class BookFinder { titleCandidates = titleCandidates.getCandidates() for (const titleCandidate of titleCandidates) { if (titleCandidate == title && authorCandidate == author) continue // We already tried this - if (++numFuzzySearches > maxFuzzySearches) return books + if (++numFuzzySearches > maxFuzzySearches) break loop_author books = await this.runSearch(titleCandidate, authorCandidate, provider, asin, maxTitleDistance, maxAuthorDistance) - if (books.length) return books + if (books.length) break loop_author } } } + if (books.length) { + const resultsHaveDuration = provider.startsWith('audible') + if (resultsHaveDuration && libraryItem && libraryItem.media?.duration) { + const libraryItemDurationMinutes = libraryItem.media.duration/60 + // If provider results have duration, sort by ascendinge duration difference from libraryItem + books.sort((a, b) => { + const aDuration = a.duration || Number.POSITIVE_INFINITY + const bDuration = b.duration || Number.POSITIVE_INFINITY + const aDurationDiff = Math.abs(aDuration - libraryItemDurationMinutes) + const bDurationDiff = Math.abs(bDuration - libraryItemDurationMinutes) + return aDurationDiff - bDurationDiff + }) + } + } return books } @@ -393,12 +409,12 @@ class BookFinder { if (provider === 'all') { for (const providerString of this.providers) { - const providerResults = await this.search(providerString, title, author, options) + const providerResults = await this.search(null, providerString, title, author, options) Logger.debug(`[BookFinder] Found ${providerResults.length} covers from ${providerString}`) searchResults.push(...providerResults) } } else { - searchResults = await this.search(provider, title, author, options) + searchResults = await this.search(null, provider, title, author, options) } Logger.debug(`[BookFinder] FindCovers search results: ${searchResults.length}`) diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 616baf29..040053e4 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -37,7 +37,7 @@ class Scanner { var searchISBN = options.isbn || libraryItem.media.metadata.isbn var searchASIN = options.asin || libraryItem.media.metadata.asin - var results = await BookFinder.search(provider, searchTitle, searchAuthor, searchISBN, searchASIN, { maxFuzzySearches: 2 }) + var results = await BookFinder.search(libraryItem, provider, searchTitle, searchAuthor, searchISBN, searchASIN, { maxFuzzySearches: 2 }) if (!results.length) { return { warning: `No ${provider} match found` diff --git a/test/server/finders/BookFinder.test.js b/test/server/finders/BookFinder.test.js index 5d28bbea..03f81f12 100644 --- a/test/server/finders/BookFinder.test.js +++ b/test/server/finders/BookFinder.test.js @@ -225,14 +225,14 @@ describe('search', () => { describe('search title is empty', () => { it('returns empty result', async () => { - expect(await bookFinder.search('', '', a)).to.deep.equal([]) + expect(await bookFinder.search(null, '', '', 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) + expect(await bookFinder.search(null, '', t, a)).to.deep.equal(r) sinon.assert.callCount(bookFinder.runSearch, 1) }) }) @@ -254,7 +254,7 @@ describe('search', () => { [`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) + expect(await bookFinder.search(null, '', searchTitle, a)).to.deep.equal(r) sinon.assert.callCount(bookFinder.runSearch, 2) }) }); @@ -264,7 +264,7 @@ describe('search', () => { [`${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) + expect(await bookFinder.search(null, '', searchTitle, a)).to.deep.equal(r) sinon.assert.callCount(bookFinder.runSearch, 3) }) }); @@ -274,7 +274,7 @@ describe('search', () => { [`${t} junk`], ].forEach(([searchTitle]) => { it(`search('${searchTitle}', '${a}') returns an empty result`, async () => { - expect(await bookFinder.search('', searchTitle, a)).to.deep.equal([]) + expect(await bookFinder.search(null, '', searchTitle, a)).to.deep.equal([]) }) }) @@ -283,7 +283,7 @@ describe('search', () => { [`${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([]) + expect(await bookFinder.search(null, '', searchTitle, a, null, null, { maxFuzzySearches: 0 })).to.deep.equal([]) sinon.assert.callCount(bookFinder.runSearch, 1) }) }) @@ -295,7 +295,7 @@ describe('search', () => { [`${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([]) + expect(await bookFinder.search(null, '', searchTitle, a, null, null, { maxFuzzySearches: 1 })).to.deep.equal([]) sinon.assert.callCount(bookFinder.runSearch, 2) }) }) @@ -308,7 +308,7 @@ describe('search', () => { [`${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) + expect(await bookFinder.search(null, '', searchTitle, '')).to.deep.equal(r) sinon.assert.callCount(bookFinder.runSearch, 2) }) }); @@ -319,7 +319,7 @@ describe('search', () => { [`${u} - ${t}`] ].forEach(([searchTitle]) => { it(`search('${searchTitle}', '') returns an empty result`, async () => { - expect(await bookFinder.search('', searchTitle, '')).to.deep.equal([]) + expect(await bookFinder.search(null, '', searchTitle, '')).to.deep.equal([]) }) }) }) @@ -330,7 +330,7 @@ describe('search', () => { [`${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) + expect(await bookFinder.search(null, '', searchTitle, u)).to.deep.equal(r) sinon.assert.callCount(bookFinder.runSearch, 2) }) }); @@ -339,9 +339,41 @@ describe('search', () => { [`${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) + expect(await bookFinder.search(null, '', searchTitle, u)).to.deep.equal(r) sinon.assert.callCount(bookFinder.runSearch, 1) }) }) }) + + describe('search provider results have duration', () => { + const libraryItem = { media: { duration: 60 * 1000 } } + const provider = 'audible' + const unsorted = [{ duration: 3000 }, { duration: 2000 }, { duration: 1000 }, { duration: 500 }] + const sorted = [{ duration: 1000 }, { duration: 500 }, { duration: 2000 }, { duration: 3000 }] + runSearchStub.withArgs(t, a, provider).resolves(unsorted) + + it('returns results sorted by library item duration diff', async () => { + expect(await bookFinder.search(libraryItem, provider, t, a)).to.deep.equal(sorted) + }) + + it('returns unsorted results if library item is null', async () => { + expect(await bookFinder.search(null, provider, t, a)).to.deep.equal(unsorted) + }) + + it('returns unsorted results if library item duration is undefined', async () => { + expect(await bookFinder.search({ media: {} }, provider, t, a)).to.deep.equal(unsorted) + }) + + it('returns unsorted results if library item media is undefined', async () => { + expect(await bookFinder.search({ }, provider, t, a)).to.deep.equal(unsorted) + }) + + it ('should return a result last if it has no duration', async () => { + const unsorted = [{}, { duration: 3000 }, { duration: 2000 }, { duration: 1000 }, { duration: 500 }] + const sorted = [{ duration: 1000 }, { duration: 500 }, { duration: 2000 }, { duration: 3000 }, {}] + runSearchStub.withArgs(t, a, provider).resolves(unsorted) + + expect(await bookFinder.search(libraryItem, provider, t, a)).to.deep.equal(sorted) + }) + }) }) From f659c3f11c2afbafd5769807111b1422effd1215 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 9 Dec 2023 13:51:28 -0600 Subject: [PATCH 0232/2145] Fix:Podcast RSS feed request header to include application/rss+xml #2401 --- server/utils/podcastUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index cf1567f9..87b080d7 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -218,7 +218,7 @@ module.exports.parsePodcastRssFeedXml = async (xml, excludeEpisodeMetadata = fal module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}"`) - return axios.get(feedUrl, { timeout: 12000, responseType: 'arraybuffer' }).then(async (data) => { + return axios.get(feedUrl, { timeout: 12000, responseType: 'arraybuffer', headers: { Accept: 'application/rss+xml' } }).then(async (data) => { // Adding support for ios-8859-1 encoded RSS feeds. // See: https://github.com/advplyr/audiobookshelf/issues/1489 From b580a23e7e05818a3d7ba38abf40f3450852eece Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 10 Dec 2023 10:35:21 -0600 Subject: [PATCH 0233/2145] BookFinder formatting update --- server/finders/BookFinder.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 8704a964..466c8701 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -354,8 +354,8 @@ class BookFinder { if (books.length) { const resultsHaveDuration = provider.startsWith('audible') - if (resultsHaveDuration && libraryItem && libraryItem.media?.duration) { - const libraryItemDurationMinutes = libraryItem.media.duration/60 + if (resultsHaveDuration && libraryItem?.media?.duration) { + const libraryItemDurationMinutes = libraryItem.media.duration / 60 // If provider results have duration, sort by ascendinge duration difference from libraryItem books.sort((a, b) => { const aDuration = a.duration || Number.POSITIVE_INFINITY @@ -472,7 +472,7 @@ 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') From 6f26fd72380e524125306691df0fda1366074040 Mon Sep 17 00:00:00 2001 From: Dmitry Naboychenko <dmitry@naboychenko.ru> Date: Tue, 12 Dec 2023 22:56:05 +0300 Subject: [PATCH 0234/2145] Update Russian localization --- client/strings/ru.json | 82 +++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/client/strings/ru.json b/client/strings/ru.json index b48e0dbd..03e7385f 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -1,10 +1,10 @@ { "ButtonAdd": "Добавить", "ButtonAddChapters": "Добавить главы", - "ButtonAddDevice": "Add Device", - "ButtonAddLibrary": "Add Library", + "ButtonAddDevice": "Добавить устройство", + "ButtonAddLibrary": "Добавить библиотеку", "ButtonAddPodcasts": "Добавить подкасты", - "ButtonAddUser": "Add User", + "ButtonAddUser": "Добавить пользователя", "ButtonAddYourFirstLibrary": "Добавьте Вашу первую библиотеку", "ButtonApply": "Применить", "ButtonApplyChapters": "Применить главы", @@ -62,7 +62,7 @@ "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": "Загрузить обложку", @@ -87,15 +87,15 @@ "ButtonUserEdit": "Редактировать пользователя {0}", "ButtonViewAll": "Посмотреть все", "ButtonYes": "Да", - "ErrorUploadFetchMetadataAPI": "Error fetching metadata", - "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", - "ErrorUploadLacksTitle": "Must have a title", + "ErrorUploadFetchMetadataAPI": "Ошибка при получении метаданных", + "ErrorUploadFetchMetadataNoResults": "Не удалось получить метаданные - попробуйте обновить название и/или автора", + "ErrorUploadLacksTitle": "Название должно быть заполнено", "HeaderAccount": "Учетная запись", "HeaderAdvanced": "Дополнительно", "HeaderAppriseNotificationSettings": "Настройки оповещений", "HeaderAudiobookTools": "Инструменты файлов аудиокниг", "HeaderAudioTracks": "Аудио треки", - "HeaderAuthentication": "Authentication", + "HeaderAuthentication": "Аутентификация", "HeaderBackups": "Бэкапы", "HeaderChangePassword": "Изменить пароль", "HeaderChapters": "Главы", @@ -130,15 +130,15 @@ "HeaderManageTags": "Редактировать теги", "HeaderMapDetails": "Найти подробности", "HeaderMatch": "Поиск", - "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", + "HeaderMetadataOrderOfPrecedence": "Порядок приоритета метаданных", "HeaderMetadataToEmbed": "Метаинформация для встраивания", "HeaderNewAccount": "Новая учетная запись", "HeaderNewLibrary": "Новая библиотека", "HeaderNotifications": "Уведомления", - "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", + "HeaderOpenIDConnectAuthentication": "Аутентификация OpenID Connect", "HeaderOpenRSSFeed": "Открыть RSS-канал", "HeaderOtherFiles": "Другие файлы", - "HeaderPasswordAuthentication": "Password Authentication", + "HeaderPasswordAuthentication": "Аутентификация по паролю", "HeaderPermissions": "Разрешения", "HeaderPlayerQueue": "Очередь воспроизведения", "HeaderPlaylist": "Плейлист", @@ -187,11 +187,11 @@ "LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию", "LabelAddToPlaylist": "Добавить в плейлист", "LabelAddToPlaylistBatch": "Добавить {0} элементов в плейлист", - "LabelAdminUsersOnly": "Admin users only", + "LabelAdminUsersOnly": "Только для пользователей с правами администратора", "LabelAll": "Все", "LabelAllUsers": "Все пользователи", - "LabelAllUsersExcludingGuests": "All users excluding guests", - "LabelAllUsersIncludingGuests": "All users including guests", + "LabelAllUsersExcludingGuests": "Все пользователи, кроме гостей", + "LabelAllUsersIncludingGuests": "Все пользователи, включая гостей", "LabelAlreadyInYourLibrary": "Уже в Вашей библиотеке", "LabelAppend": "Добавить", "LabelAuthor": "Автор", @@ -199,14 +199,14 @@ "LabelAuthorLastFirst": "Автор (Фамилия, Имя)", "LabelAuthors": "Авторы", "LabelAutoDownloadEpisodes": "Скачивать эпизоды автоматически", - "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", - "LabelAutoRegisterDescription": "Automatically create new users after logging in", + "LabelAutoFetchMetadata": "Автоматическое извлечение метаданных", + "LabelAutoFetchMetadataHelp": "Извлекает метаданные для названия, автора и серии для упрощения загрузки. После загрузки может потребоваться сопоставление дополнительных метаданных.", + "LabelAutoLaunch": "Автозапуск", + "LabelAutoLaunchDescription": "Редирект на провайдера аутентификации автоматически при переходе на страницу входа (путь ручного переопределения <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Автоматическая регистрация", + "LabelAutoRegisterDescription": "Автоматическое создание новых пользователей после входа в систему", "LabelBackToUser": "Назад к пользователю", - "LabelBackupLocation": "Backup Location", + "LabelBackupLocation": "Путь для бэкапов", "LabelBackupsEnableAutomaticBackups": "Включить автоматическое бэкапирование", "LabelBackupsEnableAutomaticBackupsHelp": "Бэкапы сохраняются в /metadata/backups", "LabelBackupsMaxBackupSize": "Максимальный размер бэкапа (в GB)", @@ -215,13 +215,13 @@ "LabelBackupsNumberToKeepHelp": "За один раз только 1 бэкап будет удален, так что если у вас будет больше бэкапов, то их нужно удалить вручную.", "LabelBitrate": "Битрейт", "LabelBooks": "Книги", - "LabelButtonText": "Button Text", + "LabelButtonText": "Текст кнопки", "LabelChangePassword": "Изменить пароль", "LabelChannels": "Каналы", "LabelChapters": "Главы", "LabelChaptersFound": "глав найдено", "LabelChapterTitle": "Название главы", - "LabelClickForMoreInfo": "Click for more info", + "LabelClickForMoreInfo": "Нажмите, чтобы узнать больше", "LabelClosePlayer": "Закрыть проигрыватель", "LabelCodec": "Кодек", "LabelCollapseSeries": "Свернуть серии", @@ -240,12 +240,12 @@ "LabelCurrently": "Текущее:", "LabelCustomCronExpression": "Пользовательское выражение Cron:", "LabelDatetime": "Дата и время", - "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", + "LabelDeleteFromFileSystemCheckbox": "Удалить из файловой системы (снимите флажок, чтобы удалить только из базы данных)", "LabelDescription": "Описание", "LabelDeselectAll": "Снять выделение", "LabelDevice": "Устройство", "LabelDeviceInfo": "Информация об устройстве", - "LabelDeviceIsAvailableTo": "Device is available to...", + "LabelDeviceIsAvailableTo": "Устройство доступно для...", "LabelDirectory": "Каталог", "LabelDiscFromFilename": "Диск из Имени файла", "LabelDiscFromMetadata": "Диск из Метаданных", @@ -271,7 +271,7 @@ "LabelExample": "Пример", "LabelExplicit": "Явный", "LabelFeedURL": "URL канала", - "LabelFetchingMetadata": "Fetching Metadata", + "LabelFetchingMetadata": "Извлечение метаданных", "LabelFile": "Файл", "LabelFileBirthtime": "Дата создания", "LabelFileModified": "Дата модификации", @@ -289,11 +289,11 @@ "LabelHardDeleteFile": "Жесткое удаление файла", "LabelHasEbook": "Есть e-книга", "LabelHasSupplementaryEbook": "Есть дополнительная e-книга", - "LabelHighestPriority": "Highest priority", + "LabelHighestPriority": "Наивысший приоритет", "LabelHost": "Хост", "LabelHour": "Часы", "LabelIcon": "Иконка", - "LabelImageURLFromTheWeb": "Image URL from the web", + "LabelImageURLFromTheWeb": "URL-адрес изображения из Интернета", "LabelIncludeInTracklist": "Включать в список воспроизведения", "LabelIncomplete": "Не завершен", "LabelInProgress": "В процессе", @@ -331,20 +331,20 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты", - "LabelLowestPriority": "Lowest Priority", - "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", + "LabelLowestPriority": "Самый низкий приоритет", + "LabelMatchExistingUsersBy": "Сопоставление существующих пользователей по", + "LabelMatchExistingUsersByDescription": "Используется для подключения существующих пользователей. После подключения пользователям будет присвоен уникальный идентификатор от поставщика единого входа", "LabelMediaPlayer": "Медиа проигрыватель", "LabelMediaType": "Тип медиа", - "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", + "LabelMetadataOrderOfPrecedenceDescription": "Источники метаданных с более высоким приоритетом будут переопределять источники метаданных с более низким приоритетом", "LabelMetadataProvider": "Провайдер", "LabelMetaTag": "Мета тег", "LabelMetaTags": "Мета теги", "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.", + "LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств", + "LabelMobileRedirectURIsDescription": "Это белый список допустимых URI перенаправления для мобильных приложений. По умолчанию используется <code>audiobookshelf://oauth</code>, который можно удалить или дополнить дополнительными URI для интеграции со сторонними приложениями. Использование звездочки (<code>*</code>) в качестве единственной записи разрешает любой URI.", "LabelMore": "Еще", "LabelMoreInfo": "Больше информации", "LabelName": "Имя", @@ -523,7 +523,7 @@ "LabelUpdateDetailsHelp": "Позволяет перезаписывать текущие подробности для выбранных книг если будут найдены", "LabelUploaderDragAndDrop": "Перетащите файлы или каталоги", "LabelUploaderDropFiles": "Перетащите файлы", - "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", + "LabelUploaderItemFetchMetadataHelp": "Автоматическое извлечение названия, автора и серии", "LabelUseChapterTrack": "Показывать время главы", "LabelUseFullTrack": "Показывать время книги", "LabelUser": "Пользователь", @@ -557,17 +557,17 @@ "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?", + "MessageConfirmDeleteLibraryItem": "Это приведет к удалению элемента библиотеки из базы данных и файловой системы. Вы уверены?", + "MessageConfirmDeleteLibraryItems": "Это приведет к удалению {0} элементов библиотеки из базы данных и файловой системы. Вы уверены?", "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?", + "MessageConfirmQuickEmbed": "Предупреждение! Быстрое встраивание не позволяет создавать резервные копии аудиофайлов. Убедитесь, что у вас есть резервная копия аудиофайлов. <br><br>Хотите продолжить?", "MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?", - "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", + "MessageConfirmRemoveAuthor": "Вы уверены, что хотите удалить автора \"{0}\"?", "MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?", "MessageConfirmRemoveEpisode": "Вы уверены, что хотите удалить эпизод \"{0}\"?", "MessageConfirmRemoveEpisodes": "Вы уверены, что хотите удалить {0} эпизодов?", @@ -579,7 +579,7 @@ "MessageConfirmRenameTag": "Вы уверены, что хотите переименовать тег \"{0}\" в \"{1}\" для всех элементов?", "MessageConfirmRenameTagMergeNote": "Примечание: Этот тег уже существует, поэтому они будут объединены.", "MessageConfirmRenameTagWarning": "Предупреждение! Похожий тег с другими начальными буквами уже существует \"{0}\".", - "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", + "MessageConfirmReScanLibraryItems": "Вы уверены, что хотите пересканировать {0} элементов?", "MessageConfirmSendEbookToDevice": "Вы уверены, что хотите отправить {0} e-книгу \"{1}\" на устройство \"{2}\"?", "MessageDownloadingEpisode": "Эпизод скачивается", "MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков", From d3256d59d56da99f2acb42ba228696e60c779a5a Mon Sep 17 00:00:00 2001 From: JBlond <leet31337@web.de> Date: Wed, 13 Dec 2023 20:12:25 +0100 Subject: [PATCH 0235/2145] - Translate more strings - Add missing least empty line --- client/strings/de.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index 3fa18960..6975b794 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -1,10 +1,10 @@ { "ButtonAdd": "Hinzufügen", "ButtonAddChapters": "Kapitel hinzufügen", - "ButtonAddDevice": "Add Device", - "ButtonAddLibrary": "Add Library", + "ButtonAddDevice": "Gerät hinzufügen", + "ButtonAddLibrary": "Bibliothek hinzufügen", "ButtonAddPodcasts": "Podcasts hinzufügen", - "ButtonAddUser": "Add User", + "ButtonAddUser": "Benutzer hinzufügen", "ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek", "ButtonApply": "Übernehmen", "ButtonApplyChapters": "Kapitel anwenden", @@ -58,11 +58,11 @@ "ButtonRemoveAll": "Alles löschen", "ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge", "ButtonRemoveFromContinueListening": "Lösche den Eintrag aus der Fortsetzungsliste", - "ButtonRemoveFromContinueReading": "Remove from Continue Reading", + "ButtonRemoveFromContinueReading": "Lösche die Serie aus der Lesefortsetzungsliste", "ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste", "ButtonReScan": "Neu scannen", "ButtonReset": "Zurücksetzen", - "ButtonResetToDefault": "Reset to default", + "ButtonResetToDefault": "Zurücksetzen auf Standard", "ButtonRestore": "Wiederherstellen", "ButtonSave": "Speichern", "ButtonSaveAndClose": "Speichern & Schließen", @@ -221,7 +221,7 @@ "LabelChapters": "Kapitel", "LabelChaptersFound": "gefundene Kapitel", "LabelChapterTitle": "Kapitelüberschrift", - "LabelClickForMoreInfo": "Click for more info", + "LabelClickForMoreInfo": "Klicken für mehr Informationen", "LabelClosePlayer": "Player schließen", "LabelCodec": "Codec", "LabelCollapseSeries": "Serien zusammenfassen", @@ -251,7 +251,7 @@ "LabelDiscFromMetadata": "CD aus den Metadaten", "LabelDiscover": "Entdecken", "LabelDownload": "Herunterladen", - "LabelDownloadNEpisodes": "Download {0} episodes", + "LabelDownloadNEpisodes": "Download {0} Episoden", "LabelDuration": "Laufzeit", "LabelDurationFound": "Gefundene Laufzeit:", "LabelEbook": "E-Book", @@ -747,4 +747,4 @@ "ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen", "ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden", "ToastUserDeleteSuccess": "Benutzer gelöscht" -} \ No newline at end of file +} From 8f7a420cca04fe15ebc5590e6a202ec5fbc40338 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 14 Dec 2023 09:47:18 +0200 Subject: [PATCH 0236/2145] Fix directory writable check (fs.access not working on Windows) --- server/managers/BinaryManager.js | 9 ++------- server/utils/fileUtils.js | 19 +++++++++++++++++++ test/server/managers/BinaryManager.test.js | 17 +++++++++-------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/server/managers/BinaryManager.js b/server/managers/BinaryManager.js index 98cb79b2..e9aab609 100644 --- a/server/managers/BinaryManager.js +++ b/server/managers/BinaryManager.js @@ -3,6 +3,7 @@ const which = require('../libs/which') const fs = require('../libs/fsExtra') const ffbinaries = require('../libs/ffbinaries') const Logger = require('../Logger') +const fileUtils = require('../utils/fileUtils') class BinaryManager { @@ -64,16 +65,10 @@ class BinaryManager { async install(binaries) { if (binaries.length == 0) return Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`) - let destination = this.mainInstallPath - try { - await fs.access(destination, fs.constants.W_OK) - } catch (err) { - destination = this.altInstallPath - } + let destination = await fileUtils.isWritable(this.mainInstallPath) ? this.mainInstallPath : this.altInstallPath await ffbinaries.downloadBinaries(binaries, { destination }) Logger.info(`[BinaryManager] Binaries installed to ${destination}`) } - } module.exports = BinaryManager \ No newline at end of file diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index ebad97db..c929068d 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -354,3 +354,22 @@ module.exports.encodeUriPath = (path) => { const uri = new URL(path, "file://") return uri.pathname } + +/** + * Check if directory is writable. + * This method is necessary because fs.access(directory, fs.constants.W_OK) does not work on Windows + * + * @param {string} directory + * @returns {boolean} + */ +module.exports.isWritable = async (directory) => { + try { + const accessTestFile = path.join(directory, 'accessTest') + await fs.writeFile(accessTestFile, '') + await fs.remove(accessTestFile) + return true + } catch (err) { + return false + } +} + diff --git a/test/server/managers/BinaryManager.test.js b/test/server/managers/BinaryManager.test.js index f9cc4df6..b93973e0 100644 --- a/test/server/managers/BinaryManager.test.js +++ b/test/server/managers/BinaryManager.test.js @@ -1,6 +1,7 @@ const chai = require('chai') const sinon = require('sinon') const fs = require('../../../server/libs/fsExtra') +const fileUtils = require('../../../server/utils/fileUtils') const which = require('../../../server/libs/which') const ffbinaries = require('../../../server/libs/ffbinaries') const path = require('path') @@ -114,19 +115,19 @@ describe('BinaryManager', () => { }) describe('install', () => { - let accessStub + let isWritableStub let downloadBinariesStub beforeEach(() => { binaryManager = new BinaryManager() - accessStub = sinon.stub(fs, 'access') + isWritableStub = sinon.stub(fileUtils, 'isWritable') downloadBinariesStub = sinon.stub(ffbinaries, 'downloadBinaries') binaryManager.mainInstallPath = '/path/to/main/install' binaryManager.altInstallPath = '/path/to/alt/install' }) afterEach(() => { - accessStub.restore() + isWritableStub.restore() downloadBinariesStub.restore() }) @@ -135,19 +136,19 @@ describe('BinaryManager', () => { await binaryManager.install(binaries) - expect(accessStub.called).to.be.false + expect(isWritableStub.called).to.be.false expect(downloadBinariesStub.called).to.be.false }) it('should install binaries in main install path if has access', async () => { const binaries = ['ffmpeg'] const destination = binaryManager.mainInstallPath - accessStub.withArgs(destination, fs.constants.W_OK).resolves() + isWritableStub.withArgs(destination).resolves(true) downloadBinariesStub.resolves() await binaryManager.install(binaries) - expect(accessStub.calledOnce).to.be.true + expect(isWritableStub.calledOnce).to.be.true expect(downloadBinariesStub.calledOnce).to.be.true expect(downloadBinariesStub.calledWith(binaries, sinon.match({ destination: destination }))).to.be.true }) @@ -156,12 +157,12 @@ describe('BinaryManager', () => { const binaries = ['ffmpeg'] const mainDestination = binaryManager.mainInstallPath const destination = binaryManager.altInstallPath - accessStub.withArgs(mainDestination, fs.constants.W_OK).rejects() + isWritableStub.withArgs(mainDestination).resolves(false) downloadBinariesStub.resolves() await binaryManager.install(binaries) - expect(accessStub.calledOnce).to.be.true + expect(isWritableStub.calledOnce).to.be.true expect(downloadBinariesStub.calledOnce).to.be.true expect(downloadBinariesStub.calledWith(binaries, sinon.match({ destination: destination }))).to.be.true }) From fae383a04520f36928b9f77ee2269cec7de26b90 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 14 Dec 2023 15:45:34 -0600 Subject: [PATCH 0237/2145] Fix:RSS feeds for collections not updating #2414 --- server/managers/RssFeedManager.js | 14 ++++++++++++-- server/models/Feed.js | 8 ++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 7eb1cce7..3149689d 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -103,19 +103,29 @@ class RssFeedManager { await Database.updateFeed(feed) } } else if (feed.entityType === 'collection') { - const collection = await Database.collectionModel.findByPk(feed.entityId) + const collection = await Database.collectionModel.findByPk(feed.entityId, { + include: Database.collectionBookModel + }) if (collection) { const collectionExpanded = await collection.getOldJsonExpanded() // Find most recently updated item in collection let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate + // Check for most recently updated book collectionExpanded.books.forEach((libraryItem) => { if (libraryItem.media.tracks.length && libraryItem.updatedAt > mostRecentlyUpdatedAt) { mostRecentlyUpdatedAt = libraryItem.updatedAt } }) + // Check for most recently added collection book + collection.collectionBooks.forEach((collectionBook) => { + if (collectionBook.createdAt.valueOf() > mostRecentlyUpdatedAt) { + mostRecentlyUpdatedAt = collectionBook.createdAt.valueOf() + } + }) + const hasBooksRemoved = collection.collectionBooks.length < feed.episodes.length - if (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt) { + if (!feed.entityUpdatedAt || hasBooksRemoved || mostRecentlyUpdatedAt > feed.entityUpdatedAt) { Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`) feed.updateFromCollection(collectionExpanded) diff --git a/server/models/Feed.js b/server/models/Feed.js index 72ea146c..d8c5a2a7 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -108,7 +108,7 @@ class Feed extends Model { /** * Find all library item ids that have an open feed (used in library filter) - * @returns {Promise<Array<String>>} array of library item ids + * @returns {Promise<string[]>} array of library item ids */ static async findAllLibraryItemIds() { const feeds = await this.findAll({ @@ -122,8 +122,8 @@ class Feed extends Model { /** * Find feed where and return oldFeed - * @param {object} where sequelize where object - * @returns {Promise<objects.Feed>} oldFeed + * @param {Object} where sequelize where object + * @returns {Promise<oldFeed>} oldFeed */ static async findOneOld(where) { if (!where) return null @@ -140,7 +140,7 @@ class Feed extends Model { /** * Find feed and return oldFeed * @param {string} id - * @returns {Promise<objects.Feed>} oldFeed + * @returns {Promise<oldFeed>} oldFeed */ static async findByPkOld(id) { if (!id) return null From 1d41904fc3400e268a8ed0f3fd26986245d8cf6e Mon Sep 17 00:00:00 2001 From: nichwall <nicholaslwallace@gmail.com> Date: Thu, 14 Dec 2023 21:04:37 -0700 Subject: [PATCH 0238/2145] Added comments to the Docker Compose file --- docker-compose.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index da3fa1f2..68e012fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,12 +3,28 @@ version: "3.7" services: audiobookshelf: - image: ghcr.io/advplyr/audiobookshelf + image: ghcr.io/advplyr/audiobookshelf:latest + # ABS runs on port 13378 by default. If you want to change + # the port, only change the external port, not the internal port ports: - 13378:80 volumes: + # These volumes are needed to keep your library persistent + # and allow media to be accessed by the ABS server. + # The path to the left of the colon is the path on your computer, + # and the path to the right of the colon is where the data is + # available to ABS in Docker. + # You can change these media directories or add as many as you want - ./audiobooks:/audiobooks - ./podcasts:/podcasts + # The metadata directory can be stored anywhere on your computer - ./metadata:/metadata + # The config directory needs to be on the same physical machine + # you are running ABS on - ./config:/config restart: unless-stopped + # You can use the following environment variable to run the ABS + # docker container as a specific user. You will need to change + # the UID and GID to the correct values for your user. + #environment: + # - user=1000:1000 From 39ceb025004b302bb4650cc382267bacfa0af36f Mon Sep 17 00:00:00 2001 From: SunX <yearnsun@gmail.com> Date: Fri, 15 Dec 2023 19:04:56 +0800 Subject: [PATCH 0239/2145] Update zh-cn.json --- client/strings/zh-cn.json | 76 +++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 7c559489..edf09040 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -1,10 +1,10 @@ { "ButtonAdd": "增加", "ButtonAddChapters": "添加章节", - "ButtonAddDevice": "Add Device", - "ButtonAddLibrary": "Add Library", + "ButtonAddDevice": "添加设备", + "ButtonAddLibrary": "添加库", "ButtonAddPodcasts": "添加播客", - "ButtonAddUser": "Add User", + "ButtonAddUser": "添加用户", "ButtonAddYourFirstLibrary": "添加第一个媒体库", "ButtonApply": "应用", "ButtonApplyChapters": "应用到章节", @@ -62,7 +62,7 @@ "ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除", "ButtonReScan": "重新扫描", "ButtonReset": "重置", - "ButtonResetToDefault": "Reset to default", + "ButtonResetToDefault": "重置为默认", "ButtonRestore": "恢复", "ButtonSave": "保存", "ButtonSaveAndClose": "保存并关闭", @@ -87,15 +87,15 @@ "ButtonUserEdit": "编辑用户 {0}", "ButtonViewAll": "查看全部", "ButtonYes": "确定", - "ErrorUploadFetchMetadataAPI": "Error fetching metadata", - "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", - "ErrorUploadLacksTitle": "Must have a title", + "ErrorUploadFetchMetadataAPI": "获取元数据时出错", + "ErrorUploadFetchMetadataNoResults": "无法获取元数据 - 尝试更新标题和/或作者", + "ErrorUploadLacksTitle": "必须有标题", "HeaderAccount": "帐户", "HeaderAdvanced": "高级", "HeaderAppriseNotificationSettings": "测试通知设置", "HeaderAudiobookTools": "有声读物文件管理工具", "HeaderAudioTracks": "音轨", - "HeaderAuthentication": "Authentication", + "HeaderAuthentication": "身份验证", "HeaderBackups": "备份", "HeaderChangePassword": "更改密码", "HeaderChapters": "章节", @@ -130,15 +130,15 @@ "HeaderManageTags": "管理标签", "HeaderMapDetails": "编辑详情", "HeaderMatch": "匹配", - "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", + "HeaderMetadataOrderOfPrecedence": "元数据优先级", "HeaderMetadataToEmbed": "嵌入元数据", "HeaderNewAccount": "新建帐户", "HeaderNewLibrary": "新建媒体库", "HeaderNotifications": "通知", - "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", + "HeaderOpenIDConnectAuthentication": "OpenID 连接身份验证", "HeaderOpenRSSFeed": "打开 RSS 源", "HeaderOtherFiles": "其他文件", - "HeaderPasswordAuthentication": "Password Authentication", + "HeaderPasswordAuthentication": "密码认证", "HeaderPermissions": "权限", "HeaderPlayerQueue": "播放队列", "HeaderPlaylist": "播放列表", @@ -187,11 +187,11 @@ "LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏", "LabelAddToPlaylist": "添加到播放列表", "LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表", - "LabelAdminUsersOnly": "Admin users only", + "LabelAdminUsersOnly": "仅限管理员用户", "LabelAll": "全部", "LabelAllUsers": "所有用户", - "LabelAllUsersExcludingGuests": "All users excluding guests", - "LabelAllUsersIncludingGuests": "All users including guests", + "LabelAllUsersExcludingGuests": "除访客外的所有用户", + "LabelAllUsersIncludingGuests": "包括访客的所有用户", "LabelAlreadyInYourLibrary": "已存在你的库中", "LabelAppend": "附加", "LabelAuthor": "作者", @@ -199,12 +199,12 @@ "LabelAuthorLastFirst": "作者 (名, 姓)", "LabelAuthors": "作者", "LabelAutoDownloadEpisodes": "自动下载剧集", - "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", - "LabelAutoRegisterDescription": "Automatically create new users after logging in", + "LabelAutoFetchMetadata": "自动获取元数据", + "LabelAutoFetchMetadataHelp": "获取标题, 作者和系列的元数据以简化上传. 上传后可能需要匹配其他元数据.", + "LabelAutoLaunch": "自动启动", + "LabelAutoLaunchDescription": "导航到登录页面时自动重定向到身份验证提供程序 (手动覆盖路径 <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "自动注册", + "LabelAutoRegisterDescription": "登录后自动创建新用户", "LabelBackToUser": "返回到用户", "LabelBackupLocation": "备份位置", "LabelBackupsEnableAutomaticBackups": "启用自动备份", @@ -215,13 +215,13 @@ "LabelBackupsNumberToKeepHelp": "一次只能删除一个备份, 因此如果你已经有超过此数量的备份, 则应手动删除它们.", "LabelBitrate": "比特率", "LabelBooks": "图书", - "LabelButtonText": "Button Text", + "LabelButtonText": "按钮文本", "LabelChangePassword": "修改密码", "LabelChannels": "声道", "LabelChapters": "章节", "LabelChaptersFound": "找到的章节", "LabelChapterTitle": "章节标题", - "LabelClickForMoreInfo": "Click for more info", + "LabelClickForMoreInfo": "点击了解更多信息", "LabelClosePlayer": "关闭播放器", "LabelCodec": "编解码", "LabelCollapseSeries": "折叠系列", @@ -240,12 +240,12 @@ "LabelCurrently": "当前:", "LabelCustomCronExpression": "自定义计划任务表达式:", "LabelDatetime": "日期时间", - "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", + "LabelDeleteFromFileSystemCheckbox": "从文件系统删除 (取消选中仅从数据库中删除)", "LabelDescription": "描述", "LabelDeselectAll": "全部取消选择", "LabelDevice": "设备", "LabelDeviceInfo": "设备信息", - "LabelDeviceIsAvailableTo": "Device is available to...", + "LabelDeviceIsAvailableTo": "设备可用于...", "LabelDirectory": "目录", "LabelDiscFromFilename": "从文件名获取光盘", "LabelDiscFromMetadata": "从元数据获取光盘", @@ -271,7 +271,7 @@ "LabelExample": "示例", "LabelExplicit": "信息准确", "LabelFeedURL": "源 URL", - "LabelFetchingMetadata": "Fetching Metadata", + "LabelFetchingMetadata": "正在获取元数据", "LabelFile": "文件", "LabelFileBirthtime": "文件创建时间", "LabelFileModified": "文件修改时间", @@ -289,7 +289,7 @@ "LabelHardDeleteFile": "完全删除文件", "LabelHasEbook": "有电子书", "LabelHasSupplementaryEbook": "有补充电子书", - "LabelHighestPriority": "Highest priority", + "LabelHighestPriority": "最高优先级", "LabelHost": "主机", "LabelHour": "小时", "LabelIcon": "图标", @@ -331,20 +331,20 @@ "LabelLogLevelInfo": "信息", "LabelLogLevelWarn": "警告", "LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集", - "LabelLowestPriority": "Lowest Priority", - "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", + "LabelLowestPriority": "最低优先级", + "LabelMatchExistingUsersBy": "匹配现有用户", + "LabelMatchExistingUsersByDescription": "用于连接现有用户. 连接后, 用户将通过SSO提供商提供的唯一 id 进行匹配", "LabelMediaPlayer": "媒体播放器", "LabelMediaType": "媒体类型", - "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", + "LabelMetadataOrderOfPrecedenceDescription": "较高优先级的元数据源将覆盖较低优先级的元数据源", "LabelMetadataProvider": "元数据提供者", "LabelMetaTag": "元数据标签", "LabelMetaTags": "元标签", "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.", + "LabelMobileRedirectURIs": "允许移动应用重定向 URI", + "LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 <code>audiobookshelf://oauth</code>,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (<code>*</code>) 作为唯一条目允许任何 URI.", "LabelMore": "更多", "LabelMoreInfo": "更多..", "LabelName": "名称", @@ -523,7 +523,7 @@ "LabelUpdateDetailsHelp": "找到匹配项时允许覆盖所选书籍存在的详细信息", "LabelUploaderDragAndDrop": "拖放文件或文件夹", "LabelUploaderDropFiles": "删除文件", - "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", + "LabelUploaderItemFetchMetadataHelp": "自动获取标题, 作者和系列", "LabelUseChapterTrack": "使用章节音轨", "LabelUseFullTrack": "使用完整音轨", "LabelUser": "用户", @@ -557,15 +557,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?", + "MessageConfirmDeleteLibraryItem": "这将从数据库和文件系统中删除库项目. 你确定吗?", + "MessageConfirmDeleteLibraryItems": "这将从数据库和文件系统中删除 {0} 个库项目. 你确定吗?", "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?", + "MessageConfirmQuickEmbed": "警告! 快速嵌入不会备份你的音频文件. 确保你有音频文件的备份. <br><br>你是否想继续吗?", "MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?", "MessageConfirmRemoveAuthor": "你确定要删除作者 \"{0}\"?", "MessageConfirmRemoveCollection": "你确定要移除收藏 \"{0}\"?", @@ -579,7 +579,7 @@ "MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?", "MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.", "MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".", - "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", + "MessageConfirmReScanLibraryItems": "你确定要重新扫描 {0} 个项目吗?", "MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?", "MessageDownloadingEpisode": "正在下载剧集", "MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序", @@ -747,4 +747,4 @@ "ToastSocketFailedToConnect": "网络连接失败", "ToastUserDeleteFailed": "删除用户失败", "ToastUserDeleteSuccess": "用户已删除" -} \ No newline at end of file +} From 728496010cbfcee5b7b54001c9f79e02ede30d82 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 17 Dec 2023 10:41:39 -0600 Subject: [PATCH 0240/2145] Update:/auth/openid/config API endpoint to require admin user and validate issuer URL --- server/Auth.js | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 0a282c9c..d2334de2 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -296,7 +296,7 @@ class Auth { 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] === '*')) { + (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) { oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/mobile-redirect`).toString() mobile_redirect_uri = req.query.redirect_uri } else { @@ -381,7 +381,7 @@ class Auth { 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') @@ -469,17 +469,38 @@ class Auth { this.handleLoginSuccessBasedOnCookie.bind(this)) /** - * Used to auto-populate the openid URLs in config/authentication + * Helper route used to auto-populate the openid URLs in config/authentication + * Takes an issuer URL as a query param and requests the config data at "/.well-known/openid-configuration" + * + * @example /auth/openid/config?issuer=http://192.168.1.66:9000/application/o/audiobookshelf/ */ - router.get('/auth/openid/config', async (req, res) => { + router.get('/auth/openid/config', this.isAuthenticated, async (req, res) => { + if (!req.user.isAdminOrUp) { + Logger.error(`[Auth] Non-admin user "${req.user.username}" attempted to get issuer config`) + return res.sendStatus(403) + } + if (!req.query.issuer) { return res.status(400).send('Invalid request. Query param \'issuer\' is required') } + + // Strip trailing slash 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 }) => { + // Append config pathname and validate URL + let configUrl = null + try { + configUrl = new URL(`${issuerUrl}/.well-known/openid-configuration`) + if (!configUrl.pathname.endsWith('/.well-known/openid-configuration')) { + throw new Error('Invalid pathname') + } + } catch (error) { + Logger.error(`[Auth] Failed to get openid configuration. Invalid URL "${configUrl}"`, error) + return res.status(400).send('Invalid request. Query param \'issuer\' is invalid') + } + + axios.get(configUrl.toString()).then(({ data }) => { res.json({ issuer: data.issuer, authorization_endpoint: data.authorization_endpoint, From 8966dbbcd1fa24825e03695a1d3fb87f01a6066e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 17 Dec 2023 11:06:03 -0600 Subject: [PATCH 0241/2145] Fix:Restrict podcast search page to admins --- client/pages/library/_library/podcast/search.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/pages/library/_library/podcast/search.vue b/client/pages/library/_library/podcast/search.vue index cde84468..3be851ce 100644 --- a/client/pages/library/_library/podcast/search.vue +++ b/client/pages/library/_library/podcast/search.vue @@ -45,6 +45,11 @@ <script> export default { async asyncData({ params, query, store, app, redirect }) { + // Podcast search/add page is restricted to admins + if (!store.getters['user/getIsAdminOrUp']) { + return redirect(`/library/${params.library}`) + } + var libraryId = params.library var libraryData = await store.dispatch('libraries/fetch', libraryId) if (!libraryData) { From 05820aa820b5ac0b9d8ce71efedae19fc8614269 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 17 Dec 2023 11:17:35 -0600 Subject: [PATCH 0242/2145] Update:API endpoints /podcasts/feed and /podcasts/opml restricted to admin users --- server/controllers/PodcastController.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index 22c3cafa..035f9152 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -16,7 +16,7 @@ class PodcastController { async create(req, res) { if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user attempted to create podcast`, req.user) + Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to create podcast`) return res.sendStatus(403) } const payload = req.body @@ -103,6 +103,11 @@ class PodcastController { } async getPodcastFeed(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get podcast feed`) + return res.sendStatus(403) + } + var url = req.body.rssFeed if (!url) { return res.status(400).send('Bad request') @@ -116,6 +121,11 @@ class PodcastController { } async getFeedsFromOPMLText(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get feeds from opml`) + return res.sendStatus(403) + } + if (!req.body.opmlText) { return res.sendStatus(400) } From dc67a5200030c31794fa5f529e647b2ea0669407 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 17 Dec 2023 11:18:21 -0600 Subject: [PATCH 0243/2145] Update:API endpoint /search/podcast throw 400 error if term query param is not supplied --- server/controllers/SearchController.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index e52e6973..34d65e3c 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -35,8 +35,19 @@ class SearchController { }) } + /** + * Find podcast RSS feeds given a term + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ async findPodcasts(req, res) { const term = req.query.term + if (!term) { + Logger.error('[SearchController] Invalid request query param "term" is required') + return res.status(400).send('Invalid request query param "term" is required') + } + const results = await PodcastFinder.search(term) res.json(results) } From f2f2ea161ca0701e1405e737b0df0f96296e4f64 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 17 Dec 2023 12:00:11 -0600 Subject: [PATCH 0244/2145] Update:API endpoint /podcasts/feed validates rssFeed URL and uses SSRF req filter --- server/controllers/PodcastController.js | 14 +++++++++++-- server/utils/index.js | 28 +++++++++++++++++++------ server/utils/podcastUtils.js | 28 ++++++++++++++++++++----- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index 035f9152..e476efd5 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -6,6 +6,7 @@ const fs = require('../libs/fsExtra') const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils') const { getFileTimestampsWithIno, filePathToPOSIX } = require('../utils/fileUtils') +const { validateUrl } = require('../utils/index') const Scanner = require('../scanner/Scanner') const CoverManager = require('../managers/CoverManager') @@ -102,15 +103,24 @@ class PodcastController { } } + /** + * POST: /api/podcasts/feed + * + * @typedef getPodcastFeedReqBody + * @property {string} rssFeed + * + * @param {import('express').Request<{}, {}, getPodcastFeedReqBody, {}} req + * @param {import('express').Response} res + */ async getPodcastFeed(req, res) { if (!req.user.isAdminOrUp) { Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get podcast feed`) return res.sendStatus(403) } - var url = req.body.rssFeed + const url = validateUrl(req.body.rssFeed) if (!url) { - return res.status(400).send('Bad request') + return res.status(400).send('Invalid request body. "rssFeed" must be a valid URL') } const podcast = await getPodcastFeed(url) diff --git a/server/utils/index.js b/server/utils/index.js index 0377b173..29a65885 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -11,24 +11,24 @@ const levenshteinDistance = (str1, str2, caseSensitive = false) => { str2 = str2.toLowerCase() } const track = Array(str2.length + 1).fill(null).map(() => - Array(str1.length + 1).fill(null)); + Array(str1.length + 1).fill(null)) for (let i = 0; i <= str1.length; i += 1) { - track[0][i] = i; + track[0][i] = i } for (let j = 0; j <= str2.length; j += 1) { - track[j][0] = j; + track[j][0] = j } for (let j = 1; j <= str2.length; j += 1) { for (let i = 1; i <= str1.length; i += 1) { - const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; + const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1 track[j][i] = Math.min( track[j][i - 1] + 1, // deletion track[j - 1][i] + 1, // insertion track[j - 1][i - 1] + indicator, // substitution - ); + ) } } - return track[str2.length][str1.length]; + return track[str2.length][str1.length] } module.exports.levenshteinDistance = levenshteinDistance @@ -204,4 +204,20 @@ module.exports.asciiOnlyToLowerCase = (str) => { module.exports.escapeRegExp = (str) => { if (typeof str !== 'string') return '' return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +/** + * Validate url string with URL class + * + * @param {string} rawUrl + * @returns {string} null if invalid + */ +module.exports.validateUrl = (rawUrl) => { + if (!rawUrl || typeof rawUrl !== 'string') return null + try { + return new URL(rawUrl).toString() + } catch (error) { + Logger.error(`Invalid URL "${rawUrl}"`, error) + return null + } } \ No newline at end of file diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 87b080d7..819ec914 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -1,5 +1,6 @@ -const Logger = require('../Logger') const axios = require('axios') +const ssrfFilter = require('ssrf-req-filter') +const Logger = require('../Logger') const { xmlToJSON, levenshteinDistance } = require('./index') const htmlSanitizer = require('../utils/htmlSanitizer') @@ -216,9 +217,26 @@ module.exports.parsePodcastRssFeedXml = async (xml, excludeEpisodeMetadata = fal } } +/** + * Get podcast RSS feed as JSON + * Uses SSRF filter to prevent internal URLs + * + * @param {string} feedUrl + * @param {boolean} [excludeEpisodeMetadata=false] + * @returns {Promise} + */ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}"`) - return axios.get(feedUrl, { timeout: 12000, responseType: 'arraybuffer', headers: { Accept: 'application/rss+xml' } }).then(async (data) => { + + return axios({ + url: feedUrl, + method: 'GET', + timeout: 12000, + responseType: 'arraybuffer', + headers: { Accept: 'application/rss+xml' }, + httpAgent: ssrfFilter(feedUrl), + httpsAgent: ssrfFilter(feedUrl) + }).then(async (data) => { // Adding support for ios-8859-1 encoded RSS feeds. // See: https://github.com/advplyr/audiobookshelf/issues/1489 @@ -231,12 +249,12 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { if (!data?.data) { Logger.error(`[podcastUtils] getPodcastFeed: Invalid podcast feed request response (${feedUrl})`) - return false + return null } Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}" success - parsing xml`) const payload = await this.parsePodcastRssFeedXml(data.data, excludeEpisodeMetadata) if (!payload) { - return false + return null } // RSS feed may be a private RSS feed @@ -245,7 +263,7 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { return payload.podcast }).catch((error) => { Logger.error('[podcastUtils] getPodcastFeed Error', error) - return false + return null }) } From 10b1784f6dc3175525900d3af689b5edb6dacc50 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 17 Dec 2023 12:23:55 -0600 Subject: [PATCH 0245/2145] Fix:Library search API endpoint /libraries/:id/search to check that query param q is a valid string --- server/controllers/LibraryController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index d2090270..70baff85 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -552,8 +552,8 @@ class LibraryController { * @param {import('express').Response} res */ async search(req, res) { - if (!req.query.q) { - return res.status(400).send('No query string') + if (!req.query.q || typeof req.query.q !== 'string') { + return res.status(400).send('Invalid request. Query param "q" must be a string') } const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12 const query = asciiOnlyToLowerCase(req.query.q.trim()) From 2d8d11d4da561b4e97cc685dc211efa00ed56a48 Mon Sep 17 00:00:00 2001 From: Trey Gordon <41927921+treyg@users.noreply.github.com> Date: Sun, 17 Dec 2023 15:56:14 -0500 Subject: [PATCH 0246/2145] docs: update synology reverse proxy to use the latest DSM settings --- readme.md | 49 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/readme.md b/readme.md index ad00dd66..3ebe097d 100644 --- a/readme.md +++ b/readme.md @@ -174,16 +174,49 @@ serve that directly: [See LinuxServer.io config sample](https://github.com/linuxserver/reverse-proxy-confs/blob/master/audiobookshelf.subdomain.conf.sample) -### Synology Reverse Proxy +### Synology NAS Reverse Proxy Setup (DSM 7+/Quickconnect) -1. Open Control Panel > Application Portal -2. Change to the Reverse Proxy tab -3. Select the proxy rule for which you want to enable Websockets and click on Edit -4. Change to the "Custom Header" tab -5. Click Create > WebSocket -6. Click Save +1. **Open Control Panel** + - Navigate to `Login Portal > Advanced`. + +2. **General Tab** + - Click `Reverse Proxy` > `Create`. + + | Setting | Value | + |---------|----------------| + | Reverse Proxy Name | audiobookshelf | + +3. **Source Configuration** + + | Setting | Value | + |-------------------------|-------------------------------------| + | Protocol | HTTPS | + | Hostname | `<sub>.<quickconnectdomain>.synology.me` | + | Port | 443 | + | Access Control Profile | Leave as is | + + - Example Hostname: `audiobookshelf.mydomain.synology.me` + +4. **Destination Configuration** + + | Setting | Value | + |-----------|------------------| + | Protocol | HTTP | + | Hostname | Your NAS IP | + | Port | 13378 | + +5. **Custom Header Tab** + - Go to `Create > Websocket`. + - Configure Headers (leave as is): + + | Header Name | Value | + |-------------|------------------| + | Upgrade | `$http_upgrade` | + | Connection | `$connection_upgrade` | + +6. **Advanced Settings Tab** + - Leave as is. -[from @silentArtifact](https://github.com/advplyr/audiobookshelf/issues/241#issuecomment-1036732329) ### [Traefik Reverse Proxy](https://doc.traefik.io/traefik/) From bef0f3709f7f9b4417f93d393049ed42327e0647 Mon Sep 17 00:00:00 2001 From: Pablo <pablo.jimenez@exheus.com> Date: Tue, 19 Dec 2023 18:39:02 +0100 Subject: [PATCH 0247/2145] feat: add basic zoom functionality to comic reader --- client/components/readers/ComicReader.vue | 34 +++++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/client/components/readers/ComicReader.vue b/client/components/readers/ComicReader.vue index 40b28a74..c0a3f957 100644 --- a/client/components/readers/ComicReader.vue +++ b/client/components/readers/ComicReader.vue @@ -26,8 +26,12 @@ <div v-if="numPages" class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20"> <p class="font-mono">{{ page }} / {{ numPages }}</p> </div> + <div class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 hidden md:flex items-center text-center"> + <ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" /> + <ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" /> + </div> - <div class="overflow-hidden w-full h-full relative"> + <div class="w-full h-full relative"> <div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent> <div class="flex items-center justify-center h-full w-1/2"> <span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span> @@ -38,10 +42,11 @@ <span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span> </div> </div> - <div class="h-full flex justify-center"> - <img v-if="mainImg" :src="mainImg" class="object-contain h-full m-auto" /> + <div class="w-full h-full relative overflow-auto"> + <div class="h-full flex" :class="scale > 100 ? '' : 'justify-center'"> + <img v-if="mainImg" :style="{ minWidth: scale + '%', width: scale + '%' }" :src="mainImg" class="object-contain m-auto" /> + </div> </div> - <div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10"> <ui-loading-indicator /> </div> @@ -54,6 +59,10 @@ import Path from 'path' import { Archive } from 'libarchive.js/main.js' import { CompressedFile } from 'libarchive.js/src/compressed-file' +// This is % with respect to the screen width +const MAX_SCALE = 400 +const MIN_SCALE = 10 + Archive.init({ workerUrl: '/libarchive/worker-bundle.js' }) @@ -81,7 +90,8 @@ export default { showInfoMenu: false, loadTimeout: null, loadedFirstPage: false, - comicMetadata: null + comicMetadata: null, + scale: 80, } }, watch: { @@ -136,6 +146,12 @@ export default { return p }) || [] ) + }, + canScaleUp() { + return this.scale < MAX_SCALE + }, + canScaleDown() { + return this.scale > MIN_SCALE } }, methods: { @@ -331,7 +347,13 @@ export default { orderedImages = orderedImages.concat(noNumImages.map((i) => i.filename)) this.pages = orderedImages - } + }, + zoomIn() { + this.scale += 10 + }, + zoomOut() { + this.scale -= 10 + }, }, mounted() {}, beforeDestroy() {} From aa7ee3e8ff3333cd9f99afc734b56e93346a6697 Mon Sep 17 00:00:00 2001 From: Pablo <pablo.jimenez@exheus.com> Date: Tue, 19 Dec 2023 18:45:11 +0100 Subject: [PATCH 0248/2145] fix: zoom buttons were showing when loading the image --- client/components/readers/ComicReader.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/readers/ComicReader.vue b/client/components/readers/ComicReader.vue index c0a3f957..57068e34 100644 --- a/client/components/readers/ComicReader.vue +++ b/client/components/readers/ComicReader.vue @@ -26,7 +26,7 @@ <div v-if="numPages" class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20"> <p class="font-mono">{{ page }} / {{ numPages }}</p> </div> - <div class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 hidden md:flex items-center text-center"> + <div v-if="mainImg" class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 hidden md:flex items-center text-center"> <ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" /> <ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" /> </div> From 7391b4d0ece9830d6d8bd0e91a2d5a56fac72af7 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 19 Dec 2023 17:19:33 -0600 Subject: [PATCH 0249/2145] Add:User stats API for year stats --- server/Database.js | 7 +- server/controllers/MeController.js | 16 +++ server/routers/ApiRouter.js | 1 + server/utils/queries/userStats.js | 178 +++++++++++++++++++++++++++++ 4 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 server/utils/queries/userStats.js diff --git a/server/Database.js b/server/Database.js index 5721ac27..8a357481 100644 --- a/server/Database.js +++ b/server/Database.js @@ -122,11 +122,16 @@ class Database { return this.models.feed } - /** @type {typeof import('./models/Feed')} */ + /** @type {typeof import('./models/FeedEpisode')} */ get feedEpisodeModel() { return this.models.feedEpisode } + /** @type {typeof import('./models/PlaybackSession')} */ + get playbackSessionModel() { + return this.models.playbackSession + } + /** * Check if db file exists * @returns {boolean} diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index d38c1c4a..42387b59 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -3,6 +3,7 @@ const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const { sort } = require('../libs/fastSort') const { toNumber } = require('../utils/index') +const userStats = require('../utils/queries/userStats') class MeController { constructor() { } @@ -333,5 +334,20 @@ class MeController { } res.json(req.user.toJSONForBrowser()) } + + /** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async getStatsForYear(req, res) { + const year = Number(req.params.year) + if (isNaN(year) || year < 2000 || year > 9999) { + Logger.error(`[MeController] Invalid year "${year}"`) + return res.status(400).send('Invalid year') + } + const data = await userStats.getStatsForYear(req.user.id, year) + res.json(data) + } } module.exports = new MeController() \ No newline at end of file diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index d7714568..f2418180 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -180,6 +180,7 @@ class ApiRouter { this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this)) this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this)) this.router.get('/me/series/:id/readd-to-continue-listening', MeController.readdSeriesFromContinueListening.bind(this)) + this.router.get('/me/year/:year/stats', MeController.getStatsForYear.bind(this)) // // Backup Routes diff --git a/server/utils/queries/userStats.js b/server/utils/queries/userStats.js new file mode 100644 index 00000000..b6895008 --- /dev/null +++ b/server/utils/queries/userStats.js @@ -0,0 +1,178 @@ +const Sequelize = require('sequelize') +const Database = require('../../Database') +const PlaybackSession = require('../../models/PlaybackSession') +const MediaProgress = require('../../models/MediaProgress') +const { elapsedPretty } = require('../index') + +module.exports = { + /** + * + * @param {string} userId + * @param {number} year YYYY + * @returns {Promise<PlaybackSession[]>} + */ + async getUserListeningSessionsForYear(userId, year) { + const sessions = await Database.playbackSessionModel.findAll({ + where: { + userId, + createdAt: { + [Sequelize.Op.gte]: `${year}-01-01`, + [Sequelize.Op.lt]: `${year + 1}-01-01` + } + } + }) + return sessions + }, + + /** + * + * @param {string} userId + * @param {number} year YYYY + * @returns {Promise<MediaProgress[]>} + */ + async getBookMediaProgressFinishedForYear(userId, year) { + const progresses = await Database.mediaProgressModel.findAll({ + where: { + userId, + mediaItemType: 'book', + finishedAt: { + [Sequelize.Op.gte]: `${year}-01-01`, + [Sequelize.Op.lt]: `${year + 1}-01-01` + } + }, + include: { + model: Database.bookModel, + required: true + } + }) + return progresses + }, + + /** + * @param {string} userId + * @param {number} year YYYY + */ + async getStatsForYear(userId, year) { + const listeningSessions = await this.getUserListeningSessionsForYear(userId, year) + + let totalBookListeningTime = 0 + let totalPodcastListeningTime = 0 + let totalListeningTime = 0 + + let authorListeningMap = {} + let genreListeningMap = {} + let narratorListeningMap = {} + let monthListeningMap = {} + + listeningSessions.forEach((ls) => { + const listeningSessionListeningTime = ls.timeListening || 0 + + const lsMonth = ls.createdAt.getMonth() + if (!monthListeningMap[lsMonth]) monthListeningMap[lsMonth] = 0 + monthListeningMap[lsMonth] += listeningSessionListeningTime + + totalListeningTime += listeningSessionListeningTime + if (ls.mediaItemType === 'book') { + totalBookListeningTime += listeningSessionListeningTime + + const authors = ls.mediaMetadata.authors || [] + authors.forEach((au) => { + if (!authorListeningMap[au.name]) authorListeningMap[au.name] = 0 + authorListeningMap[au.name] += listeningSessionListeningTime + }) + + const narrators = ls.mediaMetadata.narrators || [] + narrators.forEach((narrator) => { + if (!narratorListeningMap[narrator]) narratorListeningMap[narrator] = 0 + narratorListeningMap[narrator] += listeningSessionListeningTime + }) + + // Filter out bad genres like "audiobook" and "audio book" + const genres = (ls.mediaMetadata.genres || []).filter(g => !g.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book')) + genres.forEach((genre) => { + if (!genreListeningMap[genre]) genreListeningMap[genre] = 0 + genreListeningMap[genre] += listeningSessionListeningTime + }) + } else { + totalPodcastListeningTime += listeningSessionListeningTime + } + }) + + totalListeningTime = Math.round(totalListeningTime) + totalBookListeningTime = Math.round(totalBookListeningTime) + totalPodcastListeningTime = Math.round(totalPodcastListeningTime) + + let mostListenedAuthor = null + for (const authorName in authorListeningMap) { + if (!mostListenedAuthor?.time || authorListeningMap[authorName] > mostListenedAuthor.time) { + mostListenedAuthor = { + time: Math.round(authorListeningMap[authorName]), + pretty: elapsedPretty(Math.round(authorListeningMap[authorName])), + name: authorName + } + } + } + let mostListenedNarrator = null + for (const narrator in narratorListeningMap) { + if (!mostListenedNarrator?.time || narratorListeningMap[narrator] > mostListenedNarrator.time) { + mostListenedNarrator = { + time: Math.round(narratorListeningMap[narrator]), + pretty: elapsedPretty(Math.round(narratorListeningMap[narrator])), + name: narrator + } + } + } + let mostListenedGenre = null + for (const genre in genreListeningMap) { + if (!mostListenedGenre?.time || genreListeningMap[genre] > mostListenedGenre.time) { + mostListenedGenre = { + time: Math.round(genreListeningMap[genre]), + pretty: elapsedPretty(Math.round(genreListeningMap[genre])), + name: genre + } + } + } + let mostListenedMonth = null + for (const month in monthListeningMap) { + if (!mostListenedMonth?.time || monthListeningMap[month] > mostListenedMonth.time) { + mostListenedMonth = { + month: Number(month), + time: Math.round(monthListeningMap[month]), + pretty: elapsedPretty(Math.round(monthListeningMap[month])) + } + } + } + + const bookProgresses = await this.getBookMediaProgressFinishedForYear(userId, year) + + const numBooksFinished = bookProgresses.length + let longestAudiobookFinished = null + bookProgresses.forEach((mediaProgress) => { + if (mediaProgress.duration && (!longestAudiobookFinished?.duration || mediaProgress.duration > longestAudiobookFinished.duration)) { + longestAudiobookFinished = { + id: mediaProgress.mediaItem.id, + title: mediaProgress.mediaItem.title, + duration: Math.round(mediaProgress.duration), + durationPretty: elapsedPretty(Math.round(mediaProgress.duration)), + finishedAt: mediaProgress.finishedAt + } + } + }) + + return { + totalListeningSessions: listeningSessions.length, + totalListeningTime, + totalListeningTimePretty: elapsedPretty(totalListeningTime), + totalBookListeningTime, + totalBookListeningTimePretty: elapsedPretty(totalBookListeningTime), + totalPodcastListeningTime, + totalPodcastListeningTimePretty: elapsedPretty(totalPodcastListeningTime), + mostListenedAuthor, + mostListenedNarrator, + mostListenedGenre, + mostListenedMonth, + numBooksFinished, + longestAudiobookFinished + } + } +} From 52f0a5432b6138c705da4580ce7a4203c0cad8b0 Mon Sep 17 00:00:00 2001 From: Pablo <pablo.jimenez@exheus.com> Date: Wed, 20 Dec 2023 11:45:21 +0100 Subject: [PATCH 0250/2145] feat: enable zoom through the arrow buttons --- client/components/readers/ComicReader.vue | 34 +++++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/client/components/readers/ComicReader.vue b/client/components/readers/ComicReader.vue index 57068e34..9b09f5b6 100644 --- a/client/components/readers/ComicReader.vue +++ b/client/components/readers/ComicReader.vue @@ -26,23 +26,23 @@ <div v-if="numPages" class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20"> <p class="font-mono">{{ page }} / {{ numPages }}</p> </div> - <div v-if="mainImg" class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 hidden md:flex items-center text-center"> + <div v-if="mainImg" class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20"> <ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" /> <ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" /> </div> <div class="w-full h-full relative"> - <div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent> + <div v-show="canGoPrev" ref="prevButton" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent> <div class="flex items-center justify-center h-full w-1/2"> <span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span> </div> </div> - <div v-show="canGoNext" class="absolute top-0 right-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent> + <div v-show="canGoNext" ref="nextButton" class="absolute top-0 right-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent> <div class="flex items-center justify-center h-full w-1/2 ml-auto"> <span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span> </div> </div> - <div class="w-full h-full relative overflow-auto"> + <div ref="imageContainer" class="w-full h-full relative overflow-auto"> <div class="h-full flex" :class="scale > 100 ? '' : 'justify-center'"> <img v-if="mainImg" :style="{ minWidth: scale + '%', width: scale + '%' }" :src="mainImg" class="object-contain m-auto" /> </div> @@ -354,9 +354,31 @@ export default { zoomOut() { this.scale -= 10 }, + scroll(event) { + const imageContainer = this.$refs.imageContainer + + console.log("Scrolling by " + event.deltaY) + imageContainer.scrollBy({ + top: event.deltaY, + behavior: "auto", + }); + } }, - mounted() {}, - beforeDestroy() {} + mounted() { + const prevButton = this.$refs.prevButton + const nextButton = this.$refs.nextButton + + prevButton.addEventListener('wheel', this.scroll, { passive: false }) + nextButton.addEventListener('wheel', this.scroll, { passive: false }) + + }, + beforeDestroy() { + const prevButton = this.$refs.prevButton + const nextButton = this.$refs.nextButton + + prevButton.removeEventListener('wheel', this.scroll, { passive: false }) + nextButton.removeEventListener('wheel', this.scroll, { passive: false }) + } } </script> From 2b7122c7447943afe8d581a91bed916e1c044fdf Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 20 Dec 2023 17:18:21 -0600 Subject: [PATCH 0251/2145] Update:Year stats API endpoint & generate year in review image #2373 --- client/components/stats/YearInReview.vue | 175 +++++++++++++++++++++++ client/pages/config/stats.vue | 18 ++- server/utils/queries/userStats.js | 93 +++++++----- 3 files changed, 248 insertions(+), 38 deletions(-) create mode 100644 client/components/stats/YearInReview.vue diff --git a/client/components/stats/YearInReview.vue b/client/components/stats/YearInReview.vue new file mode 100644 index 00000000..fa57020a --- /dev/null +++ b/client/components/stats/YearInReview.vue @@ -0,0 +1,175 @@ +<template> + <div> + <div v-if="processing" class="w-[400px] h-[400px] flex items-center justify-center"> + <widgets-loading-spinner /> + </div> + <img v-else-if="dataUrl" :src="dataUrl" /> + </div> +</template> + +<script> +export default { + props: { + processing: Boolean + }, + data() { + return { + dataUrl: null, + year: null, + yearStats: null + } + }, + methods: { + async initCanvas() { + if (!this.yearStats) return + + const canvas = document.createElement('canvas') + canvas.width = 400 + canvas.height = 400 + const ctx = canvas.getContext('2d') + + const createRoundedRect = (x, y, w, h) => { + ctx.fillStyle = '#37383866' + ctx.strokeStyle = '#C0C0C0aa' + ctx.beginPath() + ctx.roundRect(x, y, w, h, [20]) + ctx.fill() + ctx.stroke() + } + + const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y) => { + ctx.fillStyle = color + ctx.font = `${fontWeight} ${fontSize} Source Sans Pro` + ctx.letterSpacing = letterSpacing + ctx.fillText(text, x, y) + } + + const addIcon = (icon, color, fontSize, x, y) => { + ctx.fillStyle = color + ctx.font = `${fontSize} Material Icons Outlined` + ctx.fillText(icon, x, y) + } + + // Bg color + ctx.fillStyle = '#232323' + ctx.fillRect(0, 0, canvas.width, canvas.height) + + // Cover image tiles + if (this.yearStats.booksWithCovers.length) { + let index = 0 + ctx.globalAlpha = 0.25 + for (let x = 0; x < 4; x++) { + for (let y = 0; y < 4; y++) { + const coverIndex = index % this.yearStats.booksWithCovers.length + let libraryItemId = this.yearStats.booksWithCovers[coverIndex] + index++ + + await new Promise((resolve) => { + const img = new Image() + img.crossOrigin = 'anonymous' + img.addEventListener('load', () => { + ctx.drawImage(img, 100 * x, 100 * y, 100, 100) + resolve() + }) + img.addEventListener('error', () => { + resolve() + }) + img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId) + }) + } + } + } + + ctx.globalAlpha = 1 + ctx.textBaseline = 'middle' + + // Create gradient + const grd1 = ctx.createLinearGradient(0, 0, 400, 400) + grd1.addColorStop(0, '#000000aa') + grd1.addColorStop(1, '#cd9d49aa') + ctx.fillStyle = grd1 + ctx.fillRect(0, 0, canvas.width, canvas.height) + + // Top Abs icon + let tanColor = '#ffdb70' + ctx.fillStyle = tanColor + ctx.font = '32px absicons' + ctx.fillText('\ue900', 15, 32) + + // Top text + addText('audiobookshelf', '22px', 'normal', tanColor, '0px', 55, 22) + addText(`${this.year} YEAR IN REVIEW`, '14px', 'bold', 'white', '1px', 55, 44) + + // Top left box + createRoundedRect(10, 65, 185, 80) + addText(this.yearStats.numBooksFinished, '32px', 'bold', 'white', '0px', 63, 98) + addText('books finished', '14px', 'normal', tanColor, '0px', 63, 120) + const readIconPath = new Path2D() + readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.2, d: 1.2, e: 26, f: 90 }) + ctx.fillStyle = '#ffffff' + ctx.fill(readIconPath) + + // Box top right + createRoundedRect(205, 65, 185, 80) + addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '20px', 'bold', 'white', '0px', 257, 96) + addText('spent listening', '14px', 'normal', tanColor, '0px', 257, 117) + addIcon('watch_later', 'white', '32px', 218, 105) + + // Box bottom left + createRoundedRect(10, 155, 185, 80) + addText(this.yearStats.totalListeningSessions, '32px', 'bold', 'white', '0px', 65, 188) + addText('sessions', '14px', 'normal', tanColor, '1px', 65, 210) + addIcon('headphones', 'white', '32px', 25, 195) + + // Box bottom right + createRoundedRect(205, 155, 185, 80) + addText(this.yearStats.numBooksListened, '32px', 'bold', 'white', '0px', 258, 188) + addText('books listened to', '14px', 'normal', tanColor, '0.65px', 258, 210) + addIcon('local_library', 'white', '32px', 220, 195) + + // Text stats + const topNarrator = this.yearStats.mostListenedNarrator + if (topNarrator) { + addText('TOP NARRATOR', '12px', 'normal', tanColor, '1px', 20, 260) + addText(topNarrator.name, '18px', 'bolder', 'white', '0px', 20, 282) + addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '14px', 'lighter', 'white', '1px', 20, 302) + } + + const topGenre = this.yearStats.topGenres[0] + if (topGenre) { + addText('TOP GENRE', '12px', 'normal', tanColor, '1px', 215, 260) + addText(topGenre.genre, '18px', 'bolder', 'white', '0px', 215, 282) + addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '14px', 'lighter', 'white', '1px', 215, 302) + } + + const topAuthor = this.yearStats.topAuthors[0] + if (topAuthor) { + addText('TOP AUTHOR', '12px', 'normal', tanColor, '1px', 20, 335) + addText(topAuthor.name, '18px', 'bolder', 'white', '0px', 20, 357) + addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '14px', 'lighter', 'white', '1px', 20, 377) + } + + this.dataUrl = canvas.toDataURL('png') + }, + refresh() { + this.init() + }, + async init() { + this.$emit('update:processing', true) + let year = new Date().getFullYear() + if (new Date().getMonth() < 11) year-- + this.year = year + this.yearStats = await this.$axios.$get(`/api/me/year/${year}/stats`).catch((err) => { + console.error('Failed to load stats for year', err) + this.$toast.error('Failed to load year stats') + return null + }) + await this.initCanvas() + this.$emit('update:processing', false) + } + }, + mounted() { + this.init() + } +} +</script> \ No newline at end of file diff --git a/client/pages/config/stats.vue b/client/pages/config/stats.vue index 9b8f7ea5..b527ea38 100644 --- a/client/pages/config/stats.vue +++ b/client/pages/config/stats.vue @@ -62,6 +62,13 @@ </div> </div> <stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" /> + + <ui-btn small :loading="processingYearInReview" @click.stop="clickShowYearInReview">Year in Review</ui-btn> + <div v-if="showYearInReview"> + <div class="w-full h-px bg-slate-200/10 my-4" /> + + <stats-year-in-review ref="yearInReview" :processing.sync="processingYearInReview" /> + </div> </app-settings-content> </div> </template> @@ -71,7 +78,9 @@ export default { data() { return { listeningStats: null, - windowWidth: 0 + windowWidth: 0, + showYearInReview: false, + processingYearInReview: false } }, watch: { @@ -114,6 +123,13 @@ export default { } }, methods: { + clickShowYearInReview() { + if (this.showYearInReview) { + this.$refs.yearInReview.refresh() + } else { + this.showYearInReview = true + } + }, async init() { this.listeningStats = await this.$axios.$get(`/api/me/listening-stats`).catch((err) => { console.error('Failed to load listening sesions', err) diff --git a/server/utils/queries/userStats.js b/server/utils/queries/userStats.js index b6895008..f9b9684e 100644 --- a/server/utils/queries/userStats.js +++ b/server/utils/queries/userStats.js @@ -2,7 +2,7 @@ const Sequelize = require('sequelize') const Database = require('../../Database') const PlaybackSession = require('../../models/PlaybackSession') const MediaProgress = require('../../models/MediaProgress') -const { elapsedPretty } = require('../index') +const fsExtra = require('../../libs/fsExtra') module.exports = { /** @@ -18,8 +18,21 @@ module.exports = { createdAt: { [Sequelize.Op.gte]: `${year}-01-01`, [Sequelize.Op.lt]: `${year + 1}-01-01` + }, + timeListening: { + [Sequelize.Op.gt]: 5 } - } + }, + include: { + model: Database.bookModel, + attributes: ['id', 'coverPath'], + include: { + model: Database.libraryItemModel, + attributes: ['id', 'mediaId', 'mediaType'] + }, + required: false + }, + order: Database.sequelize.random() }) return sessions }, @@ -42,6 +55,10 @@ module.exports = { }, include: { model: Database.bookModel, + include: { + model: Database.libraryItemModel, + attributes: ['id', 'mediaId', 'mediaType'] + }, required: true } }) @@ -63,8 +80,15 @@ module.exports = { let genreListeningMap = {} let narratorListeningMap = {} let monthListeningMap = {} + let bookListeningMap = {} + const booksWithCovers = [] + + for (const ls of listeningSessions) { + // Grab first 16 that have a cover + if (ls.mediaItem?.coverPath && !booksWithCovers.includes(ls.mediaItem.libraryItem.id) && booksWithCovers.length < 16 && await fsExtra.pathExists(ls.mediaItem.coverPath)) { + booksWithCovers.push(ls.mediaItem.libraryItem.id) + } - listeningSessions.forEach((ls) => { const listeningSessionListeningTime = ls.timeListening || 0 const lsMonth = ls.createdAt.getMonth() @@ -75,6 +99,12 @@ module.exports = { if (ls.mediaItemType === 'book') { totalBookListeningTime += listeningSessionListeningTime + if (ls.displayTitle && !bookListeningMap[ls.displayTitle]) { + bookListeningMap[ls.displayTitle] = listeningSessionListeningTime + } else if (ls.displayTitle) { + bookListeningMap[ls.displayTitle] += listeningSessionListeningTime + } + const authors = ls.mediaMetadata.authors || [] authors.forEach((au) => { if (!authorListeningMap[au.name]) authorListeningMap[au.name] = 0 @@ -96,64 +126,54 @@ module.exports = { } else { totalPodcastListeningTime += listeningSessionListeningTime } - }) + } totalListeningTime = Math.round(totalListeningTime) totalBookListeningTime = Math.round(totalBookListeningTime) totalPodcastListeningTime = Math.round(totalPodcastListeningTime) - let mostListenedAuthor = null - for (const authorName in authorListeningMap) { - if (!mostListenedAuthor?.time || authorListeningMap[authorName] > mostListenedAuthor.time) { - mostListenedAuthor = { - time: Math.round(authorListeningMap[authorName]), - pretty: elapsedPretty(Math.round(authorListeningMap[authorName])), - name: authorName - } - } - } + let topAuthors = null + topAuthors = Object.keys(authorListeningMap).map(authorName => ({ + name: authorName, + time: Math.round(authorListeningMap[authorName]) + })).sort((a, b) => b.time - a.time).slice(0, 3) + let mostListenedNarrator = null for (const narrator in narratorListeningMap) { if (!mostListenedNarrator?.time || narratorListeningMap[narrator] > mostListenedNarrator.time) { mostListenedNarrator = { time: Math.round(narratorListeningMap[narrator]), - pretty: elapsedPretty(Math.round(narratorListeningMap[narrator])), name: narrator } } } - let mostListenedGenre = null - for (const genre in genreListeningMap) { - if (!mostListenedGenre?.time || genreListeningMap[genre] > mostListenedGenre.time) { - mostListenedGenre = { - time: Math.round(genreListeningMap[genre]), - pretty: elapsedPretty(Math.round(genreListeningMap[genre])), - name: genre - } - } - } + + let topGenres = null + topGenres = Object.keys(genreListeningMap).map(genre => ({ + genre, + time: Math.round(genreListeningMap[genre]) + })).sort((a, b) => b.time - a.time).slice(0, 3) + let mostListenedMonth = null for (const month in monthListeningMap) { if (!mostListenedMonth?.time || monthListeningMap[month] > mostListenedMonth.time) { mostListenedMonth = { month: Number(month), - time: Math.round(monthListeningMap[month]), - pretty: elapsedPretty(Math.round(monthListeningMap[month])) + time: Math.round(monthListeningMap[month]) } } } - const bookProgresses = await this.getBookMediaProgressFinishedForYear(userId, year) + const bookProgressesFinished = await this.getBookMediaProgressFinishedForYear(userId, year) - const numBooksFinished = bookProgresses.length + const numBooksFinished = bookProgressesFinished.length let longestAudiobookFinished = null - bookProgresses.forEach((mediaProgress) => { + bookProgressesFinished.forEach((mediaProgress) => { if (mediaProgress.duration && (!longestAudiobookFinished?.duration || mediaProgress.duration > longestAudiobookFinished.duration)) { longestAudiobookFinished = { id: mediaProgress.mediaItem.id, title: mediaProgress.mediaItem.title, duration: Math.round(mediaProgress.duration), - durationPretty: elapsedPretty(Math.round(mediaProgress.duration)), finishedAt: mediaProgress.finishedAt } } @@ -162,17 +182,16 @@ module.exports = { return { totalListeningSessions: listeningSessions.length, totalListeningTime, - totalListeningTimePretty: elapsedPretty(totalListeningTime), totalBookListeningTime, - totalBookListeningTimePretty: elapsedPretty(totalBookListeningTime), totalPodcastListeningTime, - totalPodcastListeningTimePretty: elapsedPretty(totalPodcastListeningTime), - mostListenedAuthor, + topAuthors, + topGenres, mostListenedNarrator, - mostListenedGenre, mostListenedMonth, numBooksFinished, - longestAudiobookFinished + numBooksListened: Object.keys(bookListeningMap).length, + longestAudiobookFinished, + booksWithCovers } } } From 46ec59c74e5c9622b4053420dcfdc97f1b51d710 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 21 Dec 2023 09:44:37 -0600 Subject: [PATCH 0252/2145] Update:Year in review card prevent text overflow for narrator, author and genre #2373 --- client/components/stats/YearInReview.vue | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/client/components/stats/YearInReview.vue b/client/components/stats/YearInReview.vue index fa57020a..74c57065 100644 --- a/client/components/stats/YearInReview.vue +++ b/client/components/stats/YearInReview.vue @@ -37,10 +37,24 @@ export default { ctx.stroke() } - const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y) => { + const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => { ctx.fillStyle = color ctx.font = `${fontWeight} ${fontSize} Source Sans Pro` ctx.letterSpacing = letterSpacing + + // If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis + if (maxWidth) { + let txtWidth = ctx.measureText(text).width + while (txtWidth > maxWidth) { + console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`) + if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time + else text = text.slice(0, -3) // First check remove last 3 chars + text += '...' + txtWidth = ctx.measureText(text).width + console.log(`Checking text "${text}" (width:${txtWidth})`) + } + } + ctx.fillText(text, x, y) } @@ -131,21 +145,21 @@ export default { const topNarrator = this.yearStats.mostListenedNarrator if (topNarrator) { addText('TOP NARRATOR', '12px', 'normal', tanColor, '1px', 20, 260) - addText(topNarrator.name, '18px', 'bolder', 'white', '0px', 20, 282) + addText(topNarrator.name, '18px', 'bolder', 'white', '0px', 20, 282, 180) addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '14px', 'lighter', 'white', '1px', 20, 302) } const topGenre = this.yearStats.topGenres[0] if (topGenre) { addText('TOP GENRE', '12px', 'normal', tanColor, '1px', 215, 260) - addText(topGenre.genre, '18px', 'bolder', 'white', '0px', 215, 282) + addText(topGenre.genre, '18px', 'bolder', 'white', '0px', 215, 282, 180) addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '14px', 'lighter', 'white', '1px', 215, 302) } const topAuthor = this.yearStats.topAuthors[0] if (topAuthor) { addText('TOP AUTHOR', '12px', 'normal', tanColor, '1px', 20, 335) - addText(topAuthor.name, '18px', 'bolder', 'white', '0px', 20, 357) + addText(topAuthor.name, '18px', 'bolder', 'white', '0px', 20, 357, 180) addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '14px', 'lighter', 'white', '1px', 20, 377) } From 76119445a302f0c1109bc4fdb44100f60be7107e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 21 Dec 2023 13:52:42 -0600 Subject: [PATCH 0253/2145] Update:Listening sessions table for multi-select, sorting and rows per page - Updated get all sessions API endpoint to include sorting - Added sessions API endpoint for batch deleting --- client/components/ui/Checkbox.vue | 6 +- client/components/ui/Dropdown.vue | 4 +- client/components/ui/InputDropdown.vue | 2 +- client/pages/config/sessions.vue | 205 +++++++++++++++++++++--- client/strings/cs.json | 3 + client/strings/da.json | 3 + client/strings/de.json | 5 +- client/strings/en-us.json | 5 +- 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/sv.json | 3 + client/strings/zh-cn.json | 5 +- client/tailwind.config.js | 1 + server/controllers/SessionController.js | 137 ++++++++++++++-- server/routers/ApiRouter.js | 13 +- server/utils/index.js | 12 ++ 25 files changed, 375 insertions(+), 62 deletions(-) diff --git a/client/components/ui/Checkbox.vue b/client/components/ui/Checkbox.vue index 439d6c7d..58770aa8 100644 --- a/client/components/ui/Checkbox.vue +++ b/client/components/ui/Checkbox.vue @@ -2,7 +2,8 @@ <label class="flex justify-start items-center" :class="!disabled ? 'cursor-pointer' : ''"> <div class="border-2 rounded flex flex-shrink-0 justify-center items-center" :class="wrapperClass"> <input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" /> - <svg v-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg> + <span v-if="partial" class="material-icons text-base leading-none text-gray-400">remove</span> + <svg v-else-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg> </div> <div v-if="label" class="select-none" :class="[labelClassname, disabled ? 'text-gray-400' : 'text-gray-100']">{{ label }}</div> </label> @@ -31,7 +32,8 @@ export default { type: String, default: '' }, - disabled: Boolean + disabled: Boolean, + partial: Boolean }, data() { return {} diff --git a/client/components/ui/Dropdown.vue b/client/components/ui/Dropdown.vue index 58155499..632e38ec 100644 --- a/client/components/ui/Dropdown.vue +++ b/client/components/ui/Dropdown.vue @@ -1,6 +1,6 @@ <template> <div class="relative w-full" v-click-outside="clickOutsideObj"> - <p class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p> + <p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p> <button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu"> <span class="flex items-center"> <span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span> @@ -64,7 +64,7 @@ export default { }, itemsToShow() { return this.items.map((i) => { - if (typeof i === 'string') { + if (typeof i === 'string' || typeof i === 'number') { return { text: i, value: i diff --git a/client/components/ui/InputDropdown.vue b/client/components/ui/InputDropdown.vue index 852aa997..a9ae7467 100644 --- a/client/components/ui/InputDropdown.vue +++ b/client/components/ui/InputDropdown.vue @@ -1,6 +1,6 @@ <template> <div class="w-full"> - <label class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label> + <label v-if="label" class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label> <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'"> diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index 0b74955c..fb6e4c30 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -5,37 +5,72 @@ <ui-dropdown v-model="selectedUser" :items="userItems" :label="$strings.LabelFilterByUser" small class="max-w-48" @input="updateUserFilter" /> </div> - <div v-if="listeningSessions.length" class="block max-w-full"> + <div v-if="listeningSessions.length" class="block max-w-full relative"> <table class="userSessionsTable"> <tr class="bg-primary bg-opacity-40"> - <th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th> - <th class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</th> - <th class="w-32 min-w-32 text-left hidden md:table-cell">{{ $strings.LabelPlayMethod }}</th> - <th class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th> - <th class="w-32 min-w-32">{{ $strings.LabelTimeListened }}</th> - <th class="w-16 min-w-16">{{ $strings.LabelLastTime }}</th> - <th class="flex-grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th> + <th class="w-6 min-w-6 text-left hidden md:table-cell h-11"> + <ui-checkbox v-model="isAllSelected" :partial="numSelected > 0 && !isAllSelected" small checkbox-bg="bg" /> + </th> + <th v-if="numSelected" class="flex-grow text-left" :colspan="7"> + <div class="flex items-center"> + <p>{{ $getString('MessageSelected', [numSelected]) }}</p> + <div class="flex-grow" /> + <ui-btn small color="error" :loading="deletingSessions" @click.stop="removeSessionsClick">{{ $strings.ButtonRemove }}</ui-btn> + </div> + </th> + <th v-if="!numSelected" class="w-48 min-w-48 text-left group cursor-pointer" @click.stop="sortColumn('displayTitle')"> + <div class="inline-flex items-center"> + {{ $strings.LabelItem }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('displayTitle') }" class="material-icons text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span> + </div> + </th> + <th v-if="!numSelected" class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</th> + <th v-if="!numSelected" class="w-26 min-w-26 text-left hidden md:table-cell group cursor-pointer" @click.stop="sortColumn('playMethod')"> + <div class="inline-flex items-center"> + {{ $strings.LabelPlayMethod }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('playMethod') }" class="material-icons text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span> + </div> + </th> + <th v-if="!numSelected" class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th> + <th v-if="!numSelected" class="w-32 min-w-32 group cursor-pointer" @click.stop="sortColumn('timeListening')"> + <div class="inline-flex items-center"> + {{ $strings.LabelTimeListened }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('timeListening') }" class="material-icons text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span> + </div> + </th> + <th v-if="!numSelected" class="w-24 min-w-24 group cursor-pointer" @click.stop="sortColumn('currentTime')"> + <div class="inline-flex items-center"> + {{ $strings.LabelLastTime }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('currentTime') }" class="material-icons text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span> + </div> + </th> + <th v-if="!numSelected" class="flex-grow hidden sm:table-cell cursor-pointer group" @click.stop="sortColumn('updatedAt')"> + <div class="inline-flex items-center"> + {{ $strings.LabelLastUpdate }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('updatedAt') }" class="material-icons text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span> + </div> + </th> </tr> - <tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)"> - <td class="py-1 max-w-48"> + <tr v-for="session in listeningSessions" :key="session.id" :class="{ selected: session.selected }" class="cursor-pointer" @click="clickSessionRow(session)"> + <td class="hidden md:table-cell py-1 max-w-6 relative"> + <ui-checkbox v-model="session.selected" small checkbox-bg="bg" /> + <!-- overlay of the checkbox so that the entire box is clickable --> + <div class="absolute inset-0 w-full h-full" @click.stop="session.selected = !session.selected" /> + </td> + <td class="py-1 w-48 max-w-48"> <p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p> <p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p> </td> - <td class="hidden md:table-cell"> + <td class="hidden md:table-cell w-20 min-w-20"> <p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p> <p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p> </td> - <td class="hidden md:table-cell"> + <td class="hidden md:table-cell w-26 min-w-26"> <p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p> </td> - <td class="hidden sm:table-cell"> + <td class="hidden sm:table-cell w-32 min-w-32"> <p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" /> </td> - <td class="text-center"> + <td class="text-center w-32 min-w-32"> <p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p> </td> - <td class="text-center hover:underline" @click.stop="clickCurrentTime(session)"> + <td class="text-center hover:underline w-24 min-w-24" @click.stop="clickCurrentTime(session)"> <p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p> </td> <td class="text-center hidden sm:table-cell"> @@ -45,10 +80,22 @@ </td> </tr> </table> - <div class="flex items-center justify-end my-2"> - <ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" /> - <p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p> - <ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" /> + <!-- table bottom options --> + <div class="flex items-center my-2"> + <div class="flex-grow" /> + <div class="inline-flex items-center"> + <p class="text-sm">{{ $strings.LabelRowsPerPage }}</p> + <ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" /> + </div> + <div class="inline-flex items-center"> + <p class="text-sm mx-2">Page {{ currentPage + 1 }} of {{ numPages }}</p> + <ui-icon-btn icon="arrow_back_ios_new" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" /> + <ui-icon-btn icon="arrow_forward_ios" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" /> + </div> + </div> + + <div v-if="deletingSessions || loading" class="absolute inset-0 w-full h-full flex items-center justify-center"> + <ui-loading-indicator /> </div> </div> <p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p> @@ -128,6 +175,7 @@ export default { }, data() { return { + loading: false, showSessionModal: false, selectedSession: null, listeningSessions: [], @@ -138,7 +186,11 @@ export default { itemsPerPage: 10, userFilter: null, selectedUser: '', - processingGoToTimestamp: false + sortBy: 'updatedAt', + sortDesc: true, + processingGoToTimestamp: false, + deletingSessions: false, + itemsPerPageOptions: [10, 25, 50, 100] } }, computed: { @@ -162,9 +214,85 @@ export default { }, timeFormat() { return this.$store.state.serverSettings.timeFormat + }, + numSelected() { + return this.listeningSessions.filter((s) => s.selected).length + }, + isAllSelected: { + get() { + return this.numSelected === this.listeningSessions.length + }, + set(val) { + this.setSelectionForAll(val) + } } }, methods: { + isSortSelected(column) { + return this.sortBy === column + }, + sortColumn(column) { + if (this.sortBy === column) { + this.sortDesc = !this.sortDesc + } else { + this.sortBy = column + } + this.loadSessions(this.currentPage) + }, + removeSelectedSessions() { + if (!this.numSelected) return + this.deletingSessions = true + + let isAllSessions = this.isAllSelected + const payload = { + sessions: this.listeningSessions.filter((s) => s.selected).map((s) => s.id) + } + this.$axios + .$post(`/api/sessions/batch/delete`, payload) + .then(() => { + this.$toast.success('Sessions removed') + if (isAllSessions) { + // If all sessions were removed from the current page then go to the previous page + if (this.currentPage > 0) { + this.currentPage-- + } + this.loadSessions(this.currentPage) + } else { + // Filter out the deleted sessions + this.listeningSessions = this.listeningSessions.filter((ls) => !payload.sessions.includes(ls.id)) + } + }) + .catch((error) => { + const errorMsg = error.response?.data || 'Failed to remove sessions' + this.$toast.error(errorMsg) + }) + .finally(() => { + this.deletingSessions = false + }) + }, + removeSessionsClick() { + if (!this.numSelected) return + const payload = { + message: this.$getString('MessageConfirmRemoveListeningSessions', [this.numSelected]), + callback: (confirmed) => { + if (confirmed) { + this.removeSelectedSessions() + } + }, + type: 'yesNo' + } + this.$store.commit('globals/setConfirmPrompt', payload) + }, + setSelectionForAll(val) { + this.listeningSessions = this.listeningSessions.map((s) => { + s.selected = val + return s + }) + }, + updatedItemsPerPage() { + this.currentPage = 0 + this.loadSessions(this.currentPage) + }, closedSession() { this.loadOpenSessions() }, @@ -252,6 +380,13 @@ export default { nextPage() { this.loadSessions(this.currentPage + 1) }, + clickSessionRow(session) { + if (this.numSelected > 0) { + session.selected = !session.selected + } else { + this.showSession(session) + } + }, showSession(session) { this.selectedSession = session this.showSessionModal = true @@ -274,11 +409,21 @@ export default { return 'Unknown' }, async loadSessions(page) { - const userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : '' - const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=${this.itemsPerPage}${userFilterQuery}`).catch((err) => { + this.loading = true + const urlSearchParams = new URLSearchParams() + urlSearchParams.set('page', page) + urlSearchParams.set('itemsPerPage', this.itemsPerPage) + urlSearchParams.set('sort', this.sortBy) + urlSearchParams.set('desc', this.sortDesc ? '1' : '0') + if (this.selectedUser) { + urlSearchParams.set('user', this.selectedUser) + } + + const data = await this.$axios.$get(`/api/sessions?${urlSearchParams.toString()}`).catch((err) => { console.error('Failed to load listening sessions', err) return null }) + this.loading = false if (!data) { this.$toast.error('Failed to load listening sessions') return @@ -287,8 +432,13 @@ export default { this.numPages = data.numPages this.total = data.total this.currentPage = data.page - this.listeningSessions = data.sessions - this.userFilter = data.userFilter + this.listeningSessions = data.sessions.map((ls) => { + return { + ...ls, + selected: false + } + }) + this.userFilter = data.userId }, async loadOpenSessions() { const data = await this.$axios.$get('/api/sessions/open').catch((err) => { @@ -326,15 +476,18 @@ export default { .userSessionsTable tr:first-child { background-color: #272727; } -.userSessionsTable tr:not(:first-child) { +.userSessionsTable tr:not(:first-child):not(.selected) { background-color: #373838; } -.userSessionsTable tr:not(:first-child):nth-child(odd) { +.userSessionsTable tr:not(:first-child):nth-child(odd):not(.selected):not(:hover) { background-color: #2f2f2f; } .userSessionsTable tr:hover:not(:first-child) { background-color: #474747; } +.userSessionsTable tr.selected { + background-color: #474747; +} .userSessionsTable td { padding: 4px 8px; } diff --git a/client/strings/cs.json b/client/strings/cs.json index 6d39569e..bac376d8 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -406,6 +406,7 @@ "LabelRegion": "Region", "LabelReleaseDate": "Datum vydání", "LabelRemoveCover": "Odstranit obálku", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka", "LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka", "LabelRSSFeedOpen": "Otevření RSS kanálu", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "Opravdu chcete odstranit kolekci \"{0}\"?", "MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?", "MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "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?", @@ -650,6 +652,7 @@ "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", + "MessageSelected": "{0} selected", "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}?", diff --git a/client/strings/da.json b/client/strings/da.json index fa28dd24..3dd611d9 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -406,6 +406,7 @@ "LabelRegion": "Region", "LabelReleaseDate": "Udgivelsesdato", "LabelRemoveCover": "Fjern omslag", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail", "LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn", "LabelRSSFeedOpen": "Åben RSS-feed", @@ -571,6 +572,7 @@ "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?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "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?", @@ -650,6 +652,7 @@ "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", + "MessageSelected": "{0} selected", "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}?", diff --git a/client/strings/de.json b/client/strings/de.json index 6975b794..8cf54cbe 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -406,6 +406,7 @@ "LabelRegion": "Region", "LabelReleaseDate": "Veröffentlichungsdatum", "LabelRemoveCover": "Lösche Titelbild", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail", "LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers", "LabelRSSFeedOpen": "RSS Feed Offen", @@ -571,6 +572,7 @@ "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?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "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?", @@ -650,6 +652,7 @@ "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", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Server kann nicht erreicht werden", "MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird", "MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?", @@ -747,4 +750,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 02f9df05..f69175fd 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -406,6 +406,7 @@ "LabelRegion": "Region", "LabelReleaseDate": "Release Date", "LabelRemoveCover": "Remove cover", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedOpen": "RSS Feed Open", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?", "MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?", "MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?", "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.", "MessageSearchResultsFor": "Search results for", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Server could not be reached", "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name", "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?", @@ -747,4 +750,4 @@ "ToastSocketFailedToConnect": "Socket failed to connect", "ToastUserDeleteFailed": "Failed to delete user", "ToastUserDeleteSuccess": "User deleted" -} +} \ No newline at end of file diff --git a/client/strings/es.json b/client/strings/es.json index 47315301..cefeb8f8 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -406,6 +406,7 @@ "LabelRegion": "Región", "LabelReleaseDate": "Fecha de Estreno", "LabelRemoveCover": "Remover Portada", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado", "LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado", "LabelRSSFeedOpen": "Fuente RSS Abierta", @@ -571,6 +572,7 @@ "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?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "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?", @@ -650,6 +652,7 @@ "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.<br /><br />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.<br /><br />Todos los clientes que usen su servidor se actualizarán automáticamente.", "MessageSearchResultsFor": "Resultados de la búsqueda de", + "MessageSelected": "{0} selected", "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}?", diff --git a/client/strings/fr.json b/client/strings/fr.json index f6efa428..86a64602 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -406,6 +406,7 @@ "LabelRegion": "Région", "LabelReleaseDate": "Date de parution", "LabelRemoveCover": "Supprimer la couverture", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé", "LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé", "LabelRSSFeedOpen": "Flux RSS ouvert", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?", "MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?", "MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Êtes-vous sûr de vouloir supprimer le narrateur « {0} » ?", "MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?", "MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » en « {1} » pour tous les articles ?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "Êtes-vous certain de vouloir restaurer la sauvegarde créée le", "MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.<br /><br />Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br /><br />Tous les clients utilisant votre serveur seront automatiquement mis à jour.", "MessageSearchResultsFor": "Résultats de recherche pour", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Serveur inaccessible", "MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre", "MessageStartPlaybackAtTime": "Démarrer la lecture pour « {0} » à {1} ?", diff --git a/client/strings/gu.json b/client/strings/gu.json index 0317e2f9..24c874eb 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -406,6 +406,7 @@ "LabelRegion": "Region", "LabelReleaseDate": "Release Date", "LabelRemoveCover": "Remove cover", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedOpen": "RSS Feed Open", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?", "MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?", "MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?", "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.", "MessageSearchResultsFor": "Search results for", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Server could not be reached", "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name", "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?", diff --git a/client/strings/hi.json b/client/strings/hi.json index eb4f074f..e7ec6155 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -406,6 +406,7 @@ "LabelRegion": "Region", "LabelReleaseDate": "Release Date", "LabelRemoveCover": "Remove cover", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedOpen": "RSS Feed Open", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?", "MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?", "MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?", "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.", "MessageSearchResultsFor": "Search results for", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Server could not be reached", "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name", "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?", diff --git a/client/strings/hr.json b/client/strings/hr.json index eb7d27d8..edefcf53 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -406,6 +406,7 @@ "LabelRegion": "Regija", "LabelReleaseDate": "Datum izlaska", "LabelRemoveCover": "Remove cover", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedOpen": "RSS Feed Open", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?", "MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?", "MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?", "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "Jeste li sigurni da želite povratiti backup kreiran", "MessageRestoreBackupWarning": "Povračanje backupa će zamijeniti postoječu bazu podataka u /config i slike covera u /metadata/items i /metadata/authors.<br /><br />Backups ne modificiraju nikakve datoteke u folderu od biblioteke. Ako imate uključene server postavke da spremate cover i metapodtake u folderu od biblioteke, onda oni neće biti backupani ili overwritten.<br /><br />Svi klijenti koji koriste tvoj server će biti automatski osvježeni.", "MessageSearchResultsFor": "Traži rezultate za", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Server ne može biti kontaktiran", "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name", "MessageStartPlaybackAtTime": "Pokreni reprodukciju za \"{0}\" na {1}?", diff --git a/client/strings/it.json b/client/strings/it.json index 7e526721..0860e83f 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -406,6 +406,7 @@ "LabelRegion": "Regione", "LabelReleaseDate": "Data Release", "LabelRemoveCover": "Rimuovi cover", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Email del proprietario personalizzato", "LabelRSSFeedCustomOwnerName": "Nome del proprietario personalizzato", "LabelRSSFeedOpen": "RSS Feed Aperto", @@ -571,6 +572,7 @@ "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?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Sei sicuro di voler rimuovere il narratore \"{0}\"?", "MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?", "MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su", "MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.", "MessageSearchResultsFor": "cerca risultati per", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Impossibile raggiungere il server", "MessageSetChaptersFromTracksDescription": "Impostare i capitoli utilizzando ciascun file audio come capitolo e il titolo del capitolo come nome del file audio", "MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?", diff --git a/client/strings/lt.json b/client/strings/lt.json index 9c4b9a63..94067198 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -406,6 +406,7 @@ "LabelRegion": "Regionas", "LabelReleaseDate": "Išleidimo data", "LabelRemoveCover": "Pašalinti viršelį", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Pasirinktinis savininko el. paštas", "LabelRSSFeedCustomOwnerName": "Pasirinktinis savininko vardas", "LabelRSSFeedOpen": "Atidarytas RSS srautas", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "Ar tikrai norite pašalinti kolekciją \"{0}\"?", "MessageConfirmRemoveEpisode": "Ar tikrai norite pašalinti epizodą \"{0}\"?", "MessageConfirmRemoveEpisodes": "Ar tikrai norite pašalinti {0} epizodus?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Ar tikrai norite pašalinti skaitytoją \"{0}\"?", "MessageConfirmRemovePlaylist": "Ar tikrai norite pašalinti savo grojaraštį \"{0}\"?", "MessageConfirmRenameGenre": "Ar tikrai norite pervadinti žanrą \"{0}\" į \"{1}\" visiems elementams?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "Ar tikrai norite atkurti atsarginę kopiją, sukurtą", "MessageRestoreBackupWarning": "Atkurdami atsarginę kopiją perrašysite visą duomenų bazę, esančią /config ir viršelių vaizdus /metadata/items ir /metadata/authors.<br /><br />Atsarginės kopijos nekeičia jokių failų jūsų bibliotekos aplankuose. Jei esate įgalinę serverio nustatymus, kad viršelio meną ir metaduomenis saugotumėte savo bibliotekos aplankuose, šie neperrašomi ar atkuriami.<br /><br />Visi klientai, naudojantys jūsų serverį, bus automatiškai atnaujinti.", "MessageSearchResultsFor": "Paieškos rezultatai „{0}“", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Nepavyko pasiekti serverio", "MessageSetChaptersFromTracksDescription": "Nustatyti skyrius, naudojant kiekvieną garso failą kaip skyrių ir skyriaus pavadinimą kaip garso failo pavadinimą", "MessageStartPlaybackAtTime": "Paleisti klausymą „{0}“ nuo {1}?", diff --git a/client/strings/nl.json b/client/strings/nl.json index d4779abd..19a5c35a 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -406,6 +406,7 @@ "LabelRegion": "Regio", "LabelReleaseDate": "Verschijningsdatum", "LabelRemoveCover": "Verwijder cover", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar", "LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar", "LabelRSSFeedOpen": "RSS-feed open", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "Weet je zeker dat je de collectie \"{0}\" wil verwijderen?", "MessageConfirmRemoveEpisode": "Weet je zeker dat je de aflevering \"{0}\" wil verwijderen?", "MessageConfirmRemoveEpisodes": "Weet je zeker dat je {0} afleveringen wil verwijderen?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Weet je zeker dat je verteller \"{0}\" wil verwijderen?", "MessageConfirmRemovePlaylist": "Weet je zeker dat je je afspeellijst \"{0}\" wil verwijderen?", "MessageConfirmRenameGenre": "Weet je zeker dat je genre \"{0}\" wil hernoemen naar \"{1}\" voor alle onderdelen?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "Weet je zeker dat je wil herstellen met behulp van de back-up gemaakt op", "MessageRestoreBackupWarning": "Herstellen met een back-up zal de volledige database in /config en de covers in /metadata/items & /metadata/authors overschrijven.<br /><br />Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om covers en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven.<br /><br />Alle clients die van je server gebruik maken zullen automatisch worden ververst.", "MessageSearchResultsFor": "Zoekresultaten voor", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Server niet bereikbaar", "MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel", "MessageStartPlaybackAtTime": "Afspelen van \"{0}\" beginnen op {1}?", diff --git a/client/strings/no.json b/client/strings/no.json index 511c8b86..37cbc7a8 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -406,6 +406,7 @@ "LabelRegion": "Region", "LabelReleaseDate": "Utgivelsesdato", "LabelRemoveCover": "Fjern omslag", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Tilpasset eier Epost", "LabelRSSFeedCustomOwnerName": "Tilpasset eier Navn", "LabelRSSFeedOpen": "RSS Feed åpne", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "Er du sikker på at du vil fjerne samling\"{0}\"?", "MessageConfirmRemoveEpisode": "Er du sikker på at du vil fjerne episode \"{0}\"?", "MessageConfirmRemoveEpisodes": "Er du sikker på at du vil fjerne {0} episoder?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Er du sikker på at du vil fjerne forteller \"{0}\"?", "MessageConfirmRemovePlaylist": "Er du sikker på at du vil fjerne spillelisten \"{0}\"?", "MessageConfirmRenameGenre": "Er du sikker på at du vil endre sjanger \"{0}\" til \"{1}\" for alle gjenstandene?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "Er du sikker på at du vil gjenopprette sikkerhetskopien som var laget", "MessageRestoreBackupWarning": "gjenoppretting av sikkerhetskopi vil overskrive hele databasen under /config og omslagsbilde under /metadata/items og /metadata/authors.<br /><br />Sikkerhetskopier endrer ikke noen filer under dine bibliotekmapper. Hvis du har aktivert tjenerinstillingen for å lagre omslagsbilder og metadata i bibliotekmapper så vil ikke de filene bli tatt sikkerhetskopi eller overskrevet.<br /><br />Alle klientene som bruker din tjener vil bli fornyet automatisk.", "MessageSearchResultsFor": "Søk resultat for", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Tjener kunne ikke bli nådd", "MessageSetChaptersFromTracksDescription": "Sett kapitler ved å bruke hver lydfil som kapittel og kapitteltittel som lydfilnavnet", "MessageStartPlaybackAtTime": "Start avspilling av \"{0}\" ved {1}?", diff --git a/client/strings/pl.json b/client/strings/pl.json index b51084e9..86e29274 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -406,6 +406,7 @@ "LabelRegion": "Region", "LabelReleaseDate": "Data wydania", "LabelRemoveCover": "Remove cover", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedOpen": "RSS Feed otwarty", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "Czy na pewno chcesz usunąć kolekcję \"{0}\"?", "MessageConfirmRemoveEpisode": "Czy na pewno chcesz usunąć odcinek \"{0}\"?", "MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?", "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "Czy na pewno chcesz przywrócić kopię zapasową utworzoną w dniu", "MessageRestoreBackupWarning": "Przywrócenie kopii zapasowej spowoduje nadpisane bazy danych w folderze /config oraz okładke w folderze /metadata/items & /metadata/authors.<br /><br />Kopie zapasowe nie modyfikują żadnego pliku w folderach z plikami audio. Jeśli włączyłeś ustawienia serwera, aby przechowywać okładki i metadane w folderach biblioteki, to nie są one zapisywane w kopii zapasowej lub nadpisywane<br /><br />Wszyscy klienci korzystający z Twojego serwera będą automatycznie odświeżani", "MessageSearchResultsFor": "Wyniki wyszukiwania dla", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Nie udało się uzyskać połączenia z serwerem", "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name", "MessageStartPlaybackAtTime": "Rozpoczęcie odtwarzania \"{0}\" od {1}?", diff --git a/client/strings/ru.json b/client/strings/ru.json index 03e7385f..4d26c4aa 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -406,6 +406,7 @@ "LabelRegion": "Регион", "LabelReleaseDate": "Дата выхода", "LabelRemoveCover": "Удалить обложку", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца", "LabelRSSFeedCustomOwnerName": "Пользовательское Имя владельца", "LabelRSSFeedOpen": "Открыть RSS-канал", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?", "MessageConfirmRemoveEpisode": "Вы уверены, что хотите удалить эпизод \"{0}\"?", "MessageConfirmRemoveEpisodes": "Вы уверены, что хотите удалить {0} эпизодов?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Вы уверены, что хотите удалить чтеца \"{0}\"?", "MessageConfirmRemovePlaylist": "Вы уверены, что хотите удалить плейлист \"{0}\"?", "MessageConfirmRenameGenre": "Вы уверены, что хотите переименовать жанр \"{0}\" в \"{1}\" для всех элементов?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "Вы уверены, что хотите восстановить резервную копию, созданную", "MessageRestoreBackupWarning": "Восстановление резервной копии перезапишет всю базу данных, расположенную в /config, и обложки изображений в /metadata/items и /metadata/authors.<br/><br/>Бэкапы не изменяют файлы в папках библиотеки. Если вы включили параметры сервера для хранения обложек и метаданных в папках библиотеки, то они не резервируются и не перезаписываются.<br/><br/>Все клиенты, использующие ваш сервер, будут автоматически обновлены.", "MessageSearchResultsFor": "Результаты поиска для", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "Не удалось связаться с сервером", "MessageSetChaptersFromTracksDescription": "Установка глав с использованием каждого аудиофайла в качестве главы и заголовка главы в качестве имени аудиофайла", "MessageStartPlaybackAtTime": "Начать воспроизведение для \"{0}\" с {1}?", diff --git a/client/strings/sv.json b/client/strings/sv.json index fde0cd87..1d71fce5 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -406,6 +406,7 @@ "LabelRegion": "Region", "LabelReleaseDate": "Utgivningsdatum", "LabelRemoveCover": "Ta bort omslag", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post", "LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn", "LabelRSSFeedOpen": "Öppna RSS-flöde", @@ -571,6 +572,7 @@ "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?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "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?", @@ -650,6 +652,7 @@ "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", + "MessageSelected": "{0} selected", "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}?", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index edf09040..05553c08 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -406,6 +406,7 @@ "LabelRegion": "区域", "LabelReleaseDate": "发布日期", "LabelRemoveCover": "移除封面", + "LabelRowsPerPage": "Rows per page", "LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件", "LabelRSSFeedCustomOwnerName": "自定义所有者名称", "LabelRSSFeedOpen": "打开 RSS 源", @@ -571,6 +572,7 @@ "MessageConfirmRemoveCollection": "你确定要移除收藏 \"{0}\"?", "MessageConfirmRemoveEpisode": "你确定要移除剧集 \"{0}\"?", "MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "你确定要删除演播者 \"{0}\"?", "MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?", "MessageConfirmRenameGenre": "你确定要将所有项目流派 \"{0}\" 重命名到 \"{1}\"?", @@ -650,6 +652,7 @@ "MessageRestoreBackupConfirm": "你确定要恢复创建的这个备份", "MessageRestoreBackupWarning": "恢复备份将覆盖位于 /config 的整个数据库并覆盖 /metadata/items & /metadata/authors 中的图像.<br /><br />备份不会修改媒体库文件夹中的任何文件. 如果您已启用服务器设置将封面和元数据存储在库文件夹中,则不会备份或覆盖这些内容.<br /><br />将自动刷新使用服务器的所有客户端.", "MessageSearchResultsFor": "搜索结果", + "MessageSelected": "{0} selected", "MessageServerCouldNotBeReached": "无法访问服务器", "MessageSetChaptersFromTracksDescription": "把每个音频文件设置为章节并将章节标题设置为音频文件名", "MessageStartPlaybackAtTime": "开始播放 \"{0}\" 在 {1}?", @@ -747,4 +750,4 @@ "ToastSocketFailedToConnect": "网络连接失败", "ToastUserDeleteFailed": "删除用户失败", "ToastUserDeleteSuccess": "用户已删除" -} +} \ No newline at end of file diff --git a/client/tailwind.config.js b/client/tailwind.config.js index 1c39cb8f..9eb81923 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -56,6 +56,7 @@ module.exports = { '16': '4rem', '20': '5rem', '24': '6rem', + '26': '6.5rem', '32': '8rem', '48': '12rem', '64': '16rem', diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 884f0cd6..22fcaa1c 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -1,6 +1,6 @@ const Logger = require('../Logger') const Database = require('../Database') -const { toNumber } = require('../utils/index') +const { toNumber, isUUID } = require('../utils/index') class SessionController { constructor() { } @@ -9,35 +9,97 @@ class SessionController { return res.json(req.playbackSession) } + /** + * GET: /api/sessions + * @this import('../routers/ApiRouter') + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ async getAllWithUserData(req, res) { if (!req.user.isAdminOrUp) { Logger.error(`[SessionController] getAllWithUserData: Non-admin user requested all session data ${req.user.id}/"${req.user.username}"`) return res.sendStatus(404) } - - let listeningSessions = [] - if (req.query.user) { - listeningSessions = await this.getUserListeningSessionsHelper(req.query.user) - } else { - listeningSessions = await this.getAllSessionsWithUserData() + // Validate "user" query + let userId = req.query.user + if (userId && !isUUID(userId)) { + Logger.warn(`[SessionController] Invalid "user" query string "${userId}"`) + userId = null + } + // Validate "sort" query + const validSortOrders = ['displayTitle', 'duration', 'playMethod', 'startTime', 'currentTime', 'timeListening', 'updatedAt', 'createdAt'] + let orderKey = req.query.sort || 'updatedAt' + if (!validSortOrders.includes(orderKey)) { + Logger.warn(`[SessionController] Invalid "sort" query string "${orderKey}" (Must be one of "${validSortOrders.join('|')}")`) + orderKey = 'updatedAt' + } + let orderDesc = req.query.desc === '1' ? 'DESC' : 'ASC' + // Validate "itemsPerPage" and "page" query + let itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10 + if (itemsPerPage < 1) { + Logger.warn(`[SessionController] Invalid "itemsPerPage" query string "${itemsPerPage}"`) + itemsPerPage = 10 + } + let page = toNumber(req.query.page, 0) + if (page < 0) { + Logger.warn(`[SessionController] Invalid "page" query string "${page}"`) + page = 0 } - const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10 - const page = toNumber(req.query.page, 0) + let where = null + const include = [ + { + model: Database.models.device + } + ] - const start = page * itemsPerPage - const sessions = listeningSessions.slice(start, start + itemsPerPage) + if (userId) { + where = { + userId + } + } else { + include.push({ + model: Database.userModel, + attributes: ['id', 'username'] + }) + } + + const { rows, count } = await Database.playbackSessionModel.findAndCountAll({ + where, + include, + order: [ + [orderKey, orderDesc] + ], + limit: itemsPerPage, + offset: itemsPerPage * page + }) + + // Map playback sessions to old playback sessions + const sessions = rows.map(session => { + const oldPlaybackSession = Database.playbackSessionModel.getOldPlaybackSession(session) + if (session.user) { + return { + ...oldPlaybackSession, + user: { + id: session.user.id, + username: session.user.username + } + } + } else { + return oldPlaybackSession.toJSON() + } + }) const payload = { - total: listeningSessions.length, - numPages: Math.ceil(listeningSessions.length / itemsPerPage), + total: count, + numPages: Math.ceil(count / itemsPerPage), page, itemsPerPage, sessions } - - if (req.query.user) { - payload.userFilter = req.query.user + if (userId) { + payload.userId = userId } res.json(payload) @@ -92,6 +154,49 @@ class SessionController { res.sendStatus(200) } + /** + * POST: /api/sessions/batch/delete + * @this import('../routers/ApiRouter') + * + * @typedef batchDeleteReqBody + * @property {string[]} sessions + * + * @param {import('express').Request<{}, {}, batchDeleteReqBody, {}} req + * @param {import('express').Response} res + */ + async batchDelete(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[SessionController] Non-admin user attempted to batch delete sessions "${req.user.username}"`) + return res.sendStatus(403) + } + // Validate session ids + if (!req.body.sessions?.length || !Array.isArray(req.body.sessions) || req.body.sessions.some(s => !isUUID(s))) { + Logger.error(`[SessionController] Invalid request body. "sessions" array is required`, req.body) + return res.status(400).send('Invalid request body. "sessions" array of session id strings is required.') + } + + // Check if any of these sessions are open and close it + for (const sessionId of req.body.sessions) { + const openSession = this.playbackSessionManager.getSession(sessionId) + if (openSession) { + await this.playbackSessionManager.removeSession(sessionId) + } + } + + try { + const sessionsRemoved = await Database.playbackSessionModel.destroy({ + where: { + id: req.body.sessions + } + }) + Logger.info(`[SessionController] ${sessionsRemoved} playback sessions removed by "${req.user.username}"`) + res.sendStatus(200) + } catch (error) { + Logger.error(`[SessionController] Failed to remove playback sessions`, error) + res.status(500).send('Failed to remove sessions') + } + } + // POST: api/session/local syncLocal(req, res) { this.playbackSessionManager.syncLocalSessionRequest(req, res) diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index f2418180..42e1d040 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -221,6 +221,7 @@ class ApiRouter { this.router.get('/sessions', SessionController.getAllWithUserData.bind(this)) this.router.delete('/sessions/:id', SessionController.middleware.bind(this), SessionController.delete.bind(this)) this.router.get('/sessions/open', SessionController.getOpenSessions.bind(this)) + this.router.post('/sessions/batch/delete', SessionController.batchDelete.bind(this)) this.router.post('/session/local', SessionController.syncLocal.bind(this)) this.router.post('/session/local-all', SessionController.syncLocalSessions.bind(this)) // TODO: Update these endpoints because they are only for open playback sessions @@ -491,18 +492,6 @@ class ApiRouter { return userSessions.sort((a, b) => b.updatedAt - a.updatedAt) } - async getAllSessionsWithUserData() { - const sessions = await Database.getPlaybackSessions() - sessions.sort((a, b) => b.updatedAt - a.updatedAt) - const minifiedUserObjects = await Database.userModel.getMinifiedUserObjects() - return sessions.map(se => { - return { - ...se, - user: minifiedUserObjects.find(u => u.id === se.userId) || null - } - }) - } - async getUserListeningStatsHelpers(userId) { const today = date.format(new Date(), 'YYYY-MM-DD') diff --git a/server/utils/index.js b/server/utils/index.js index 29a65885..f75572ba 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -1,4 +1,5 @@ const Path = require('path') +const uuid = require('uuid') const Logger = require('../Logger') const { parseString } = require("xml2js") const areEquivalent = require('./areEquivalent') @@ -220,4 +221,15 @@ module.exports.validateUrl = (rawUrl) => { Logger.error(`Invalid URL "${rawUrl}"`, error) return null } +} + +/** + * Check if a string is a valid UUID + * + * @param {string} str + * @returns {boolean} + */ +module.exports.isUUID = (str) => { + if (!str || typeof str !== 'string') return false + return uuid.validate(str) } \ No newline at end of file From 24a587b944bce4111817c05336d751e2a789cb93 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 21 Dec 2023 14:29:36 -0600 Subject: [PATCH 0254/2145] Update:Remove playback sessions that are 3s or less on startup --- server/Database.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/server/Database.js b/server/Database.js index 8a357481..fd606bac 100644 --- a/server/Database.js +++ b/server/Database.js @@ -1,5 +1,5 @@ const Path = require('path') -const { Sequelize } = require('sequelize') +const { Sequelize, Op } = require('sequelize') const packageJson = require('../package.json') const fs = require('./libs/fsExtra') @@ -698,6 +698,7 @@ class Database { * Clean invalid records in database * Series should have atleast one Book * Book and Podcast must have an associated LibraryItem + * Remove playback sessions that are 3 seconds or less */ async cleanDatabase() { // Remove invalid Podcast records @@ -738,6 +739,18 @@ class Database { Logger.warn(`Found series "${series.name}" with no books - removing it`) await series.destroy() } + + // Remove playback sessions that were 3 seconds or less + const badSessionsRemoved = await this.playbackSessionModel.destroy({ + where: { + timeListening: { + [Op.lte]: 3 + } + } + }) + if (badSessionsRemoved > 0) { + Logger.warn(`Removed ${badSessionsRemoved} sessions that were 3 seconds or less`) + } } } From 68d36522b1bfa6da591d818bc99725b26b95dd0c Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 21 Dec 2023 14:36:51 -0600 Subject: [PATCH 0255/2145] Update:Listening sessions table UI for mobile --- client/pages/config/sessions.vue | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index fb6e4c30..d90d849d 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -18,7 +18,7 @@ <ui-btn small color="error" :loading="deletingSessions" @click.stop="removeSessionsClick">{{ $strings.ButtonRemove }}</ui-btn> </div> </th> - <th v-if="!numSelected" class="w-48 min-w-48 text-left group cursor-pointer" @click.stop="sortColumn('displayTitle')"> + <th v-if="!numSelected" class="flex-grow sm:flex-grow-0 sm:w-48 sm:max-w-48 text-left group cursor-pointer" @click.stop="sortColumn('displayTitle')"> <div class="inline-flex items-center"> {{ $strings.LabelItem }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('displayTitle') }" class="material-icons text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span> </div> @@ -30,14 +30,14 @@ </div> </th> <th v-if="!numSelected" class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th> - <th v-if="!numSelected" class="w-32 min-w-32 group cursor-pointer" @click.stop="sortColumn('timeListening')"> + <th v-if="!numSelected" class="w-24 min-w-24 sm:w-32 sm:min-w-32 group cursor-pointer" @click.stop="sortColumn('timeListening')"> <div class="inline-flex items-center"> - {{ $strings.LabelTimeListened }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('timeListening') }" class="material-icons text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span> + {{ $strings.LabelTimeListened }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('timeListening') }" class="material-icons text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span> </div> </th> <th v-if="!numSelected" class="w-24 min-w-24 group cursor-pointer" @click.stop="sortColumn('currentTime')"> <div class="inline-flex items-center"> - {{ $strings.LabelLastTime }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('currentTime') }" class="material-icons text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span> + {{ $strings.LabelLastTime }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('currentTime') }" class="material-icons text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span> </div> </th> <th v-if="!numSelected" class="flex-grow hidden sm:table-cell cursor-pointer group" @click.stop="sortColumn('updatedAt')"> @@ -53,7 +53,7 @@ <!-- overlay of the checkbox so that the entire box is clickable --> <div class="absolute inset-0 w-full h-full" @click.stop="session.selected = !session.selected" /> </td> - <td class="py-1 w-48 max-w-48"> + <td class="py-1 flex-grow sm:flex-grow-0 sm:w-48 sm:max-w-48"> <p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p> <p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p> </td> @@ -67,7 +67,7 @@ <td class="hidden sm:table-cell w-32 min-w-32"> <p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" /> </td> - <td class="text-center w-32 min-w-32"> + <td class="text-center w-24 min-w-24 sm:w-32 sm:min-w-32"> <p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p> </td> <td class="text-center hover:underline w-24 min-w-24" @click.stop="clickCurrentTime(session)"> @@ -83,7 +83,7 @@ <!-- table bottom options --> <div class="flex items-center my-2"> <div class="flex-grow" /> - <div class="inline-flex items-center"> + <div class="hidden sm:inline-flex items-center"> <p class="text-sm">{{ $strings.LabelRowsPerPage }}</p> <ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" /> </div> From 2738402aacbe43f21061667b417da35ceb8d5c10 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 22 Dec 2023 17:01:07 -0600 Subject: [PATCH 0256/2145] Add:Year in review card for server stats #2373 --- client/components/stats/YearInReview.vue | 89 ++++---- .../components/stats/YearInReviewServer.vue | 205 ++++++++++++++++++ client/pages/config/stats.vue | 11 +- server/controllers/MeController.js | 3 +- server/controllers/MiscController.js | 21 ++ server/routers/ApiRouter.js | 3 +- server/utils/queries/adminStats.js | 118 ++++++++++ server/utils/queries/userStats.js | 12 +- 8 files changed, 414 insertions(+), 48 deletions(-) create mode 100644 client/components/stats/YearInReviewServer.vue create mode 100644 server/utils/queries/adminStats.js diff --git a/client/components/stats/YearInReview.vue b/client/components/stats/YearInReview.vue index 74c57065..104392b4 100644 --- a/client/components/stats/YearInReview.vue +++ b/client/components/stats/YearInReview.vue @@ -24,12 +24,15 @@ export default { if (!this.yearStats) return const canvas = document.createElement('canvas') - canvas.width = 400 - canvas.height = 400 + canvas.width = 800 + canvas.height = 800 const ctx = canvas.getContext('2d') const createRoundedRect = (x, y, w, h) => { - ctx.fillStyle = '#37383866' + const grd1 = ctx.createLinearGradient(x, y, x + w, y + h) + grd1.addColorStop(0, '#44444466') + grd1.addColorStop(1, '#ffffff22') + ctx.fillStyle = grd1 ctx.strokeStyle = '#C0C0C0aa' ctx.beginPath() ctx.roundRect(x, y, w, h, [20]) @@ -72,8 +75,13 @@ export default { if (this.yearStats.booksWithCovers.length) { let index = 0 ctx.globalAlpha = 0.25 - for (let x = 0; x < 4; x++) { - for (let y = 0; y < 4; y++) { + ctx.save() + ctx.translate(canvas.width / 2, canvas.height / 2) + ctx.rotate((-Math.PI / 180) * 25) + ctx.translate(-canvas.width / 2, -canvas.height / 2) + ctx.translate(-130, -120) + for (let x = 0; x < 5; x++) { + for (let y = 0; y < 5; y++) { const coverIndex = index % this.yearStats.booksWithCovers.length let libraryItemId = this.yearStats.booksWithCovers[coverIndex] index++ @@ -82,7 +90,13 @@ export default { const img = new Image() img.crossOrigin = 'anonymous' img.addEventListener('load', () => { - ctx.drawImage(img, 100 * x, 100 * y, 100, 100) + let sw = img.width + if (img.width > img.height) { + sw = img.height + } + let sx = -(sw - img.width) / 2 + let sy = -(sw - img.height) / 2 + ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215) resolve() }) img.addEventListener('error', () => { @@ -92,13 +106,14 @@ export default { }) } } + ctx.restore() } ctx.globalAlpha = 1 ctx.textBaseline = 'middle' // Create gradient - const grd1 = ctx.createLinearGradient(0, 0, 400, 400) + const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height) grd1.addColorStop(0, '#000000aa') grd1.addColorStop(1, '#cd9d49aa') ctx.fillStyle = grd1 @@ -107,60 +122,60 @@ export default { // Top Abs icon let tanColor = '#ffdb70' ctx.fillStyle = tanColor - ctx.font = '32px absicons' - ctx.fillText('\ue900', 15, 32) + ctx.font = '42px absicons' + ctx.fillText('\ue900', 15, 36) // Top text - addText('audiobookshelf', '22px', 'normal', tanColor, '0px', 55, 22) - addText(`${this.year} YEAR IN REVIEW`, '14px', 'bold', 'white', '1px', 55, 44) + addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28) + addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51) // Top left box - createRoundedRect(10, 65, 185, 80) - addText(this.yearStats.numBooksFinished, '32px', 'bold', 'white', '0px', 63, 98) - addText('books finished', '14px', 'normal', tanColor, '0px', 63, 120) + createRoundedRect(50, 100, 340, 160) + addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165) + addText('books finished', '28px', 'normal', tanColor, '0px', 160, 210) const readIconPath = new Path2D() - readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.2, d: 1.2, e: 26, f: 90 }) + readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 2, d: 2, e: 100, f: 160 }) ctx.fillStyle = '#ffffff' ctx.fill(readIconPath) // Box top right - createRoundedRect(205, 65, 185, 80) - addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '20px', 'bold', 'white', '0px', 257, 96) - addText('spent listening', '14px', 'normal', tanColor, '0px', 257, 117) - addIcon('watch_later', 'white', '32px', 218, 105) + createRoundedRect(410, 100, 340, 160) + addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165) + addText('spent listening', '28px', 'normal', tanColor, '0.5px', 500, 205) + addIcon('watch_later', 'white', '52px', 440, 180) // Box bottom left - createRoundedRect(10, 155, 185, 80) - addText(this.yearStats.totalListeningSessions, '32px', 'bold', 'white', '0px', 65, 188) - addText('sessions', '14px', 'normal', tanColor, '1px', 65, 210) - addIcon('headphones', 'white', '32px', 25, 195) + createRoundedRect(50, 280, 340, 160) + addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345) + addText('sessions', '28px', 'normal', tanColor, '1px', 160, 390) + addIcon('headphones', 'white', '52px', 95, 360) // Box bottom right - createRoundedRect(205, 155, 185, 80) - addText(this.yearStats.numBooksListened, '32px', 'bold', 'white', '0px', 258, 188) - addText('books listened to', '14px', 'normal', tanColor, '0.65px', 258, 210) - addIcon('local_library', 'white', '32px', 220, 195) + createRoundedRect(410, 280, 340, 160) + addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345) + addText('books listened to', '28px', 'normal', tanColor, '0.5px', 500, 390) + addIcon('local_library', 'white', '52px', 440, 360) // Text stats const topNarrator = this.yearStats.mostListenedNarrator if (topNarrator) { - addText('TOP NARRATOR', '12px', 'normal', tanColor, '1px', 20, 260) - addText(topNarrator.name, '18px', 'bolder', 'white', '0px', 20, 282, 180) - addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '14px', 'lighter', 'white', '1px', 20, 302) + addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520) + addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330) + addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599) } const topGenre = this.yearStats.topGenres[0] if (topGenre) { - addText('TOP GENRE', '12px', 'normal', tanColor, '1px', 215, 260) - addText(topGenre.genre, '18px', 'bolder', 'white', '0px', 215, 282, 180) - addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '14px', 'lighter', 'white', '1px', 215, 302) + addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520) + addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330) + addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599) } const topAuthor = this.yearStats.topAuthors[0] if (topAuthor) { - addText('TOP AUTHOR', '12px', 'normal', tanColor, '1px', 20, 335) - addText(topAuthor.name, '18px', 'bolder', 'white', '0px', 20, 357, 180) - addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '14px', 'lighter', 'white', '1px', 20, 377) + addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670) + addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330) + addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749) } this.dataUrl = canvas.toDataURL('png') @@ -173,7 +188,7 @@ export default { let year = new Date().getFullYear() if (new Date().getMonth() < 11) year-- this.year = year - this.yearStats = await this.$axios.$get(`/api/me/year/${year}/stats`).catch((err) => { + this.yearStats = await this.$axios.$get(`/api/me/stats/year/${year}`).catch((err) => { console.error('Failed to load stats for year', err) this.$toast.error('Failed to load year stats') return null diff --git a/client/components/stats/YearInReviewServer.vue b/client/components/stats/YearInReviewServer.vue new file mode 100644 index 00000000..0d1fa8aa --- /dev/null +++ b/client/components/stats/YearInReviewServer.vue @@ -0,0 +1,205 @@ +<template> + <div> + <div v-if="processing" class="w-[400px] h-[400px] flex items-center justify-center"> + <widgets-loading-spinner /> + </div> + <img v-else-if="dataUrl" :src="dataUrl" /> + </div> +</template> + +<script> +export default { + props: { + processing: Boolean + }, + data() { + return { + dataUrl: null, + year: null, + yearStats: null + } + }, + methods: { + async initCanvas() { + if (!this.yearStats) return + + const canvas = document.createElement('canvas') + canvas.width = 800 + canvas.height = 800 + const ctx = canvas.getContext('2d') + + const createRoundedRect = (x, y, w, h) => { + const grd1 = ctx.createLinearGradient(x, y, x + w, y + h) + grd1.addColorStop(0, '#44444466') + grd1.addColorStop(1, '#ffffff22') + ctx.fillStyle = grd1 + ctx.strokeStyle = '#C0C0C0aa' + ctx.beginPath() + ctx.roundRect(x, y, w, h, [20]) + ctx.fill() + ctx.stroke() + } + + const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => { + ctx.fillStyle = color + ctx.font = `${fontWeight} ${fontSize} Source Sans Pro` + ctx.letterSpacing = letterSpacing + + // If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis + if (maxWidth) { + let txtWidth = ctx.measureText(text).width + while (txtWidth > maxWidth) { + console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`) + if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time + else text = text.slice(0, -3) // First check remove last 3 chars + text += '...' + txtWidth = ctx.measureText(text).width + console.log(`Checking text "${text}" (width:${txtWidth})`) + } + } + + ctx.fillText(text, x, y) + } + + const addIcon = (icon, color, fontSize, x, y) => { + ctx.fillStyle = color + ctx.font = `${fontSize} Material Icons Outlined` + ctx.fillText(icon, x, y) + } + + // Bg color + ctx.fillStyle = '#232323' + ctx.fillRect(0, 0, canvas.width, canvas.height) + + // Cover image tiles + let imgsToAdd = {} + + if (this.yearStats.booksAddedWithCovers.length) { + let index = 0 + ctx.globalAlpha = 0.25 + ctx.save() + ctx.translate(canvas.width / 2, canvas.height / 2) + ctx.rotate((-Math.PI / 180) * 25) + ctx.translate(-canvas.width / 2, -canvas.height / 2) + ctx.translate(-130, -120) + for (let x = 0; x < 5; x++) { + for (let y = 0; y < 5; y++) { + const coverIndex = index % this.yearStats.booksAddedWithCovers.length + let libraryItemId = this.yearStats.booksAddedWithCovers[coverIndex] + index++ + + await new Promise((resolve) => { + const img = new Image() + img.crossOrigin = 'anonymous' + img.addEventListener('load', () => { + let sw = img.width + if (img.width > img.height) { + sw = img.height + } + let sx = -(sw - img.width) / 2 + let sy = -(sw - img.height) / 2 + ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215) + if (!imgsToAdd[libraryItemId]) { + imgsToAdd[libraryItemId] = { + img, + sx, + sy, + sw + } + } + resolve() + }) + img.addEventListener('error', () => { + resolve() + }) + img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId) + }) + } + } + ctx.restore() + } + + ctx.globalAlpha = 1 + ctx.textBaseline = 'middle' + + // Create gradient + const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height) + grd1.addColorStop(0, '#000000aa') + grd1.addColorStop(1, '#cd9d49aa') + ctx.fillStyle = grd1 + ctx.fillRect(0, 0, canvas.width, canvas.height) + + // Top Abs icon + let tanColor = '#ffdb70' + ctx.fillStyle = tanColor + ctx.font = '42px absicons' + ctx.fillText('\ue900', 15, 36) + + // Top text + addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28) + addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51) + + // Top left box + createRoundedRect(40, 100, 230, 100) + ctx.textAlign = 'center' + addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140) + addText('books added', '18px', 'normal', tanColor, '0px', 155, 170) + + // Box top right + createRoundedRect(285, 100, 230, 100) + addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140) + addText('authors added', '18px', 'normal', tanColor, '0px', 400, 170) + + // Box bottom left + createRoundedRect(530, 100, 230, 100) + addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140) + addText('sessions', '18px', 'normal', tanColor, '1px', 645, 170) + + // Text stats + if (this.yearStats.totalBooksAddedSize) { + addText('Your book collection grew to...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 260) + addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300) + addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330) + } + + if (this.yearStats.totalBooksAddedDuration) { + addText('With a total duration of...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 400) + addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440) + addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470) + } + + // Bottom images + imgsToAdd = Object.values(imgsToAdd) + if (imgsToAdd.length >= 5) { + addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540) + + for (let i = 0; i < 5; i++) { + let imgToAdd = imgsToAdd[i] + ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 580, 140, 140) + } + } + + this.dataUrl = canvas.toDataURL('png') + }, + refresh() { + this.init() + }, + async init() { + this.$emit('update:processing', true) + let year = new Date().getFullYear() + if (new Date().getMonth() < 11) year-- + this.year = year + this.yearStats = await this.$axios.$get(`/api/stats/year/${year}`).catch((err) => { + console.error('Failed to load stats for year', err) + this.$toast.error('Failed to load year stats') + return null + }) + await this.initCanvas() + this.$emit('update:processing', false) + } + }, + mounted() { + this.init() + } +} +</script> \ No newline at end of file diff --git a/client/pages/config/stats.vue b/client/pages/config/stats.vue index b527ea38..96581714 100644 --- a/client/pages/config/stats.vue +++ b/client/pages/config/stats.vue @@ -63,11 +63,13 @@ </div> <stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" /> - <ui-btn small :loading="processingYearInReview" @click.stop="clickShowYearInReview">Year in Review</ui-btn> + <ui-btn small :loading="processingYearInReview || processingYearInReviewAlt" @click.stop="clickShowYearInReview">{{ showYearInReview ? 'Refresh Year in Review' : 'Year in Review' }}</ui-btn> <div v-if="showYearInReview"> <div class="w-full h-px bg-slate-200/10 my-4" /> <stats-year-in-review ref="yearInReview" :processing.sync="processingYearInReview" /> + + <stats-year-in-review-server v-if="isAdminOrUp" ref="yearInReviewAlt" :processing.sync="processingYearInReviewAlt" /> </div> </app-settings-content> </div> @@ -80,7 +82,8 @@ export default { listeningStats: null, windowWidth: 0, showYearInReview: false, - processingYearInReview: false + processingYearInReview: false, + processingYearInReviewAlt: false } }, watch: { @@ -126,6 +129,10 @@ export default { clickShowYearInReview() { if (this.showYearInReview) { this.$refs.yearInReview.refresh() + + if (this.$refs.yearInReviewAlt) { + this.$refs.yearInReviewAlt.refresh() + } } else { this.showYearInReview = true } diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 42387b59..8fa5c6bc 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -336,6 +336,7 @@ class MeController { } /** + * GET: /api/stats/year/:year * * @param {import('express').Request} req * @param {import('express').Response} res @@ -346,7 +347,7 @@ class MeController { Logger.error(`[MeController] Invalid year "${year}"`) return res.status(400).send('Invalid year') } - const data = await userStats.getStatsForYear(req.user.id, year) + const data = await userStats.getStatsForYear(req.user, year) res.json(data) } } diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index db4110e0..c2272ee6 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -11,6 +11,7 @@ const { isObject, getTitleIgnorePrefix } = require('../utils/index') const { sanitizeFilename } = require('../utils/fileUtils') const TaskManager = require('../managers/TaskManager') +const adminStats = require('../utils/queries/adminStats') // // This is a controller for routes that don't have a home yet :( @@ -696,5 +697,25 @@ class MiscController { serverSettings: Database.serverSettings.toJSONForBrowser() }) } + + /** + * GET: /api/me/stats/year/:year + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async getAdminStatsForYear(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get admin stats for year`) + return res.sendStatus(403) + } + const year = Number(req.params.year) + if (isNaN(year) || year < 2000 || year > 9999) { + Logger.error(`[MiscController] Invalid year "${year}"`) + return res.status(400).send('Invalid year') + } + const stats = await adminStats.getStatsForYear(year) + res.json(stats) + } } module.exports = new MiscController() diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 42e1d040..3edce256 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -180,7 +180,7 @@ class ApiRouter { this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this)) this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this)) this.router.get('/me/series/:id/readd-to-continue-listening', MeController.readdSeriesFromContinueListening.bind(this)) - this.router.get('/me/year/:year/stats', MeController.getStatsForYear.bind(this)) + this.router.get('/me/stats/year/:year', MeController.getStatsForYear.bind(this)) // // Backup Routes @@ -317,6 +317,7 @@ class ApiRouter { 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)) + this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this)) } async getDirectories(dir, relpath, excludedDirs, level = 0) { diff --git a/server/utils/queries/adminStats.js b/server/utils/queries/adminStats.js new file mode 100644 index 00000000..66a31e8d --- /dev/null +++ b/server/utils/queries/adminStats.js @@ -0,0 +1,118 @@ +const Sequelize = require('sequelize') +const Database = require('../../Database') +const PlaybackSession = require('../../models/PlaybackSession') +const fsExtra = require('../../libs/fsExtra') + +module.exports = { + /** + * + * @param {number} year YYYY + * @returns {Promise<PlaybackSession[]>} + */ + async getListeningSessionsForYear(year) { + const sessions = await Database.playbackSessionModel.findAll({ + where: { + createdAt: { + [Sequelize.Op.gte]: `${year}-01-01`, + [Sequelize.Op.lt]: `${year + 1}-01-01` + } + } + }) + return sessions + }, + + /** + * + * @param {number} year YYYY + * @returns {Promise<number>} + */ + async getNumAuthorsAddedForYear(year) { + const count = await Database.authorModel.count({ + where: { + createdAt: { + [Sequelize.Op.gte]: `${year}-01-01`, + [Sequelize.Op.lt]: `${year + 1}-01-01` + } + } + }) + return count + }, + + /** + * + * @param {number} year YYYY + * @returns {Promise<import('../../models/Book')[]>} + */ + async getBooksAddedForYear(year) { + const books = await Database.bookModel.findAll({ + attributes: ['id', 'title', 'coverPath', 'duration', 'createdAt'], + where: { + createdAt: { + [Sequelize.Op.gte]: `${year}-01-01`, + [Sequelize.Op.lt]: `${year + 1}-01-01` + } + }, + include: { + model: Database.libraryItemModel, + attributes: ['id', 'mediaId', 'mediaType', 'size'], + required: true + }, + order: Database.sequelize.random() + }) + return books + }, + + /** + * + * @param {number} year YYYY + */ + async getStatsForYear(year) { + const booksAdded = await this.getBooksAddedForYear(year) + + let totalBooksAddedSize = 0 + let totalBooksAddedDuration = 0 + const booksWithCovers = [] + + for (const book of booksAdded) { + // Grab first 25 that have a cover + if (book.coverPath && !booksWithCovers.includes(book.libraryItem.id) && booksWithCovers.length < 25 && await fsExtra.pathExists(book.coverPath)) { + booksWithCovers.push(book.libraryItem.id) + } + if (book.duration && !isNaN(book.duration)) { + totalBooksAddedDuration += book.duration + } + if (book.libraryItem.size && !isNaN(book.libraryItem.size)) { + totalBooksAddedSize += book.libraryItem.size + } + } + + const numAuthorsAdded = await this.getNumAuthorsAddedForYear(year) + + const listeningSessions = await this.getListeningSessionsForYear(year) + let totalListeningTime = 0 + for (const listeningSession of listeningSessions) { + totalListeningTime += (listeningSession.timeListening || 0) + } + + // Stats for total books, size and duration for everything added this year or earlier + const [totalStatResultsRow] = await Database.sequelize.query(`SELECT SUM(li.size) AS totalSize, SUM(b.duration) AS totalDuration, COUNT(*) AS totalItems FROM libraryItems li, books b WHERE b.id = li.mediaId AND li.mediaType = 'book' AND li.createdAt < ":nextYear-01-01";`, { + replacements: { + nextYear: year + 1 + } + }) + const totalStatResults = totalStatResultsRow[0] + + return { + numListeningSessions: listeningSessions.length, + numBooksAdded: booksAdded.length, + numAuthorsAdded, + totalBooksAddedSize, + totalBooksAddedDuration: Math.round(totalBooksAddedDuration), + booksAddedWithCovers: booksWithCovers, + totalBooksSize: totalStatResults?.totalSize || 0, + totalBooksDuration: totalStatResults?.totalDuration || 0, + totalListeningTime, + numBooks: totalStatResults?.totalItems || 0 + } + } +} diff --git a/server/utils/queries/userStats.js b/server/utils/queries/userStats.js index f9b9684e..0f997789 100644 --- a/server/utils/queries/userStats.js +++ b/server/utils/queries/userStats.js @@ -18,9 +18,6 @@ module.exports = { createdAt: { [Sequelize.Op.gte]: `${year}-01-01`, [Sequelize.Op.lt]: `${year + 1}-01-01` - }, - timeListening: { - [Sequelize.Op.gt]: 5 } }, include: { @@ -66,10 +63,11 @@ module.exports = { }, /** - * @param {string} userId + * @param {import('../../objects/user/User')} user * @param {number} year YYYY */ - async getStatsForYear(userId, year) { + async getStatsForYear(user, year) { + const userId = user.id const listeningSessions = await this.getUserListeningSessionsForYear(userId, year) let totalBookListeningTime = 0 @@ -84,8 +82,8 @@ module.exports = { const booksWithCovers = [] for (const ls of listeningSessions) { - // Grab first 16 that have a cover - if (ls.mediaItem?.coverPath && !booksWithCovers.includes(ls.mediaItem.libraryItem.id) && booksWithCovers.length < 16 && await fsExtra.pathExists(ls.mediaItem.coverPath)) { + // Grab first 25 that have a cover + if (ls.mediaItem?.coverPath && !booksWithCovers.includes(ls.mediaItem.libraryItem.id) && booksWithCovers.length < 25 && await fsExtra.pathExists(ls.mediaItem.coverPath)) { booksWithCovers.push(ls.mediaItem.libraryItem.id) } From 47bf9f7836ec04e9ebb4a05735b5611d0ac9859e Mon Sep 17 00:00:00 2001 From: JBlond <leet31337@web.de> Date: Sat, 23 Dec 2023 14:42:56 +0100 Subject: [PATCH 0257/2145] Follow up Translations for 76119445a302f0c1109bc4fdb44100f60be7107e * Update:Listening sessions table for multi-select, sorting and rows per page - Updated get all sessions API endpoint to include sorting - Added sessions API endpoint for batch deleting --- client/strings/de.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index 8cf54cbe..20da77c1 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -406,7 +406,7 @@ "LabelRegion": "Region", "LabelReleaseDate": "Veröffentlichungsdatum", "LabelRemoveCover": "Lösche Titelbild", - "LabelRowsPerPage": "Rows per page", + "LabelRowsPerPage": "Zeilen pro Seite", "LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail", "LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers", "LabelRSSFeedOpen": "RSS Feed Offen", @@ -572,7 +572,7 @@ "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?", - "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", + "MessageConfirmRemoveListeningSessions": "Sind Sie sicher, dass sie {0} Hörsitzungen enfernen möchten?", "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?", @@ -652,7 +652,7 @@ "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", - "MessageSelected": "{0} selected", + "MessageSelected": "{0} ausgewählt", "MessageServerCouldNotBeReached": "Server kann nicht erreicht werden", "MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird", "MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?", @@ -750,4 +750,4 @@ "ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen", "ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden", "ToastUserDeleteSuccess": "Benutzer gelöscht" -} \ No newline at end of file +} From 72fa6b8200fb66ade979cbaf2479d92bd972ea83 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 23 Dec 2023 10:50:04 -0600 Subject: [PATCH 0258/2145] Fix:Show cover size widget when audio player is open #2443 --- client/components/app/BookShelfCategorized.vue | 5 ++++- client/components/app/LazyBookshelf.vue | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index 15f34867..eb4e9424 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -1,7 +1,7 @@ <template> <div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative"> <!-- Cover size widget --> - <widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" /> + <widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" /> <div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12"> <p class="text-center text-2xl mb-4 py-4">{{ libraryName }} Library is empty!</p> @@ -94,6 +94,9 @@ export default { }, selectedMediaItems() { return this.$store.state.globals.selectedMediaItems || [] + }, + streamLibraryItem() { + return this.$store.state.streamLibraryItem } }, methods: { diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue index 189f2c83..5291cdbb 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -21,7 +21,7 @@ </div> </div> - <widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" /> + <widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" /> </div> </template> @@ -205,6 +205,9 @@ export default { sizeMultiplier() { const baseSize = this.isCoverSquareAspectRatio ? 192 : 120 return this.entityWidth / baseSize + }, + streamLibraryItem() { + return this.$store.state.streamLibraryItem } }, methods: { From 0d644fe0c995f7f94ba7a4947241bbe16db898f7 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 23 Dec 2023 15:29:34 -0600 Subject: [PATCH 0259/2145] Add:Year in review banner for user stats page #2373 --- client/components/stats/YearInReview.vue | 143 +++++++++++---- .../components/stats/YearInReviewBanner.vue | 134 ++++++++++++++ .../components/stats/YearInReviewServer.vue | 99 +++++++--- client/components/stats/YearInReviewShort.vue | 169 ++++++++++++++++++ client/pages/config/stats.vue | 38 ++-- server/utils/queries/adminStats.js | 50 +++++- server/utils/queries/userStats.js | 49 +++-- 7 files changed, 583 insertions(+), 99 deletions(-) create mode 100644 client/components/stats/YearInReviewBanner.vue create mode 100644 client/components/stats/YearInReviewShort.vue diff --git a/client/components/stats/YearInReview.vue b/client/components/stats/YearInReview.vue index 104392b4..3fef3a8c 100644 --- a/client/components/stats/YearInReview.vue +++ b/client/components/stats/YearInReview.vue @@ -1,24 +1,34 @@ <template> <div> - <div v-if="processing" class="w-[400px] h-[400px] flex items-center justify-center"> + <div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center"> <widgets-loading-spinner /> </div> - <img v-else-if="dataUrl" :src="dataUrl" /> + <img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" /> </div> </template> <script> export default { props: { + variant: { + type: Number, + default: 0 + }, + year: Number, processing: Boolean }, data() { return { + canvas: null, dataUrl: null, - year: null, yearStats: null } }, + watch: { + variant() { + this.init() + } + }, methods: { async initCanvas() { if (!this.yearStats) return @@ -72,7 +82,12 @@ export default { ctx.fillRect(0, 0, canvas.width, canvas.height) // Cover image tiles - if (this.yearStats.booksWithCovers.length) { + const bookCovers = this.yearStats.finishedBooksWithCovers + bookCovers.push(...this.yearStats.booksWithCovers) + + let finishedBookCoverImgs = {} + + if (bookCovers.length) { let index = 0 ctx.globalAlpha = 0.25 ctx.save() @@ -82,8 +97,8 @@ export default { ctx.translate(-130, -120) for (let x = 0; x < 5; x++) { for (let y = 0; y < 5; y++) { - const coverIndex = index % this.yearStats.booksWithCovers.length - let libraryItemId = this.yearStats.booksWithCovers[coverIndex] + const coverIndex = index % bookCovers.length + let libraryItemId = bookCovers[coverIndex] index++ await new Promise((resolve) => { @@ -98,6 +113,14 @@ export default { let sy = -(sw - img.height) / 2 ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215) resolve() + if (this.yearStats.finishedBooksWithCovers.includes(libraryItemId) && !finishedBookCoverImgs[libraryItemId]) { + finishedBookCoverImgs[libraryItemId] = { + img, + sx, + sy, + sw + } + } }) img.addEventListener('error', () => { resolve() @@ -141,7 +164,7 @@ export default { // Box top right createRoundedRect(410, 100, 340, 160) addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165) - addText('spent listening', '28px', 'normal', tanColor, '0.5px', 500, 205) + addText('spent listening', '28px', 'normal', tanColor, '0px', 500, 205) addIcon('watch_later', 'white', '52px', 440, 180) // Box bottom left @@ -153,42 +176,98 @@ export default { // Box bottom right createRoundedRect(410, 280, 340, 160) addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345) - addText('books listened to', '28px', 'normal', tanColor, '0.5px', 500, 390) + addText('books listened to', '28px', 'normal', tanColor, '0px', 500, 390) addIcon('local_library', 'white', '52px', 440, 360) - // Text stats - const topNarrator = this.yearStats.mostListenedNarrator - if (topNarrator) { - addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520) - addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330) - addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599) - } - - const topGenre = this.yearStats.topGenres[0] - if (topGenre) { - addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520) - addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330) - addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599) - } - - const topAuthor = this.yearStats.topAuthors[0] - if (topAuthor) { - addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670) - addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330) - addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749) + if (!this.variant) { + // Text stats + const topNarrator = this.yearStats.mostListenedNarrator + if (topNarrator) { + addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520) + addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330) + addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599) + } + + const topGenre = this.yearStats.topGenres[0] + if (topGenre) { + addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520) + addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330) + addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599) + } + + const topAuthor = this.yearStats.topAuthors[0] + if (topAuthor) { + addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670) + addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330) + addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749) + } + + if (this.yearStats.mostListenedMonth?.time) { + const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1) + const monthName = this.$formatJsDate(jsdate, 'LLLL') + addText('TOP MONTH', '24px', 'normal', tanColor, '1px', 430, 670) + addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330) + addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749) + } + } else if (this.variant === 1) { + // Bottom images + finishedBookCoverImgs = Object.values(finishedBookCoverImgs) + if (finishedBookCoverImgs.length > 0) { + ctx.textAlign = 'center' + addText('Some books finished this year...', '28px', 'normal', tanColor, '0px', canvas.width / 2, 530) + + for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) { + let imgToAdd = finishedBookCoverImgs[i] + ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 570, 140, 140) + } + } + } else if (this.variant === 2) { + // Text stats + if (this.yearStats.topAuthors.length) { + addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 524) + for (let i = 0; i < this.yearStats.topAuthors.length; i++) { + addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330) + } + } + + if (this.yearStats.topGenres.length) { + addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 524) + for (let i = 0; i < this.yearStats.topGenres.length; i++) { + addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330) + } + } } + this.canvas = canvas this.dataUrl = canvas.toDataURL('png') }, refresh() { this.init() }, + share() { + this.canvas.toBlob((blob) => { + const file = new File([blob], 'yearinreview.png', { type: blob.type }) + const shareData = { + files: [file] + } + if (navigator.canShare(shareData)) { + navigator + .share(shareData) + .then(() => { + console.log('Share success') + }) + .catch((error) => { + console.error('Failed to share', error) + this.$toast.error('Failed to share: ' + error.message) + }) + } else { + this.$toast.error('Cannot share natively on this device') + } + }) + }, async init() { this.$emit('update:processing', true) - let year = new Date().getFullYear() - if (new Date().getMonth() < 11) year-- - this.year = year - this.yearStats = await this.$axios.$get(`/api/me/stats/year/${year}`).catch((err) => { + this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => { console.error('Failed to load stats for year', err) this.$toast.error('Failed to load year stats') return null diff --git a/client/components/stats/YearInReviewBanner.vue b/client/components/stats/YearInReviewBanner.vue new file mode 100644 index 00000000..9a0a0bbf --- /dev/null +++ b/client/components/stats/YearInReviewBanner.vue @@ -0,0 +1,134 @@ +<template> + <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-4"> + <!-- hack to get icon fonts loaded on init --> + <div class="h-0 w-0 overflow-hidden opacity-0"> + <span class="material-icons-outlined">close</span> + <span class="abs-icons icon-audiobookshelf" /> + </div> + + <div class="flex items-center"> + <p class="hidden md:block text-xl font-semibold">{{ yearInReviewYear }} Year in Review</p> + <div class="hidden md:block flex-grow" /> + <ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? 'Hide Year in Review' : 'See Year in Review' }}</ui-btn> + </div> + + <!-- your year in review --> + <div v-if="showYearInReview"> + <div class="w-full h-px bg-slate-200/10 my-4" /> + + <div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto"> + <!-- previous button --> + <ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--"> + <span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> + <span class="hidden sm:inline-block pr-2">Previous</span> + </ui-btn> + <!-- share button --> + <ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview"> Share </ui-btn> + + <div class="flex-grow" /> + <p class="text-lg font-semibold">Your Year <span class="hidden md:inline-block">in Review </span>({{ yearInReviewVariant + 1 }})</p> + <div class="flex-grow" /> + + <!-- refresh button --> + <ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview"> + <span class="hidden sm:inline-block">Refresh</span> + <span class="material-icons sm:!hidden text-lg py-px">refresh</span> + </ui-btn> + <!-- next button --> + <ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++"> + <span class="hidden sm:inline-block pl-2">Next</span> + <span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> + </ui-btn> + </div> + <stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" /> + + <!-- your year in review short --> + <div class="w-full max-w-[800px] mx-auto my-4"> + <stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" /> + </div> + + <!-- your server in review --> + <div v-if="isAdminOrUp" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10"> + <div class="flex items-center justify-center mb-2"> + <!-- previous button --> + <ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--"> + <span class="material-icons-outlined text-lg px-1 sm:pr-1 py-px sm:py-0">chevron_left</span> + <span class="hidden sm:inline-block pr-2">Previous</span> + </ui-btn> + <!-- share button --> + <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer"> Share </ui-btn> + + <div class="flex-grow" /> + <p class="text-lg font-semibold">Server <span class="hidden md:inline-block">Year in Review </span>({{ yearInReviewServerVariant + 1 }})</p> + <div class="flex-grow" /> + + <!-- refresh button --> + <ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer"> + <span class="hidden sm:inline-block">Refresh</span> + <span class="material-icons sm:!hidden text-lg py-px">refresh</span> + </ui-btn> + <!-- next button --> + <ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++"> + <span class="hidden sm:inline-block pl-2">Next</span> + <span class="material-icons-outlined text-lg px-1 sm:pl-1 py-px sm:py-0">chevron_right</span> + </ui-btn> + </div> + </div> + <stats-year-in-review-server v-if="isAdminOrUp" ref="yearInReviewServer" :year="yearInReviewYear" :variant="yearInReviewServerVariant" :processing.sync="processingYearInReviewServer" /> + </div> + </div> +</template> + +<script> +export default { + data() { + return { + showYearInReview: false, + yearInReviewYear: 0, + yearInReviewVariant: 0, + yearInReviewServerVariant: 0, + processingYearInReview: false, + processingYearInReviewShort: false, + processingYearInReviewServer: false, + showShareButton: false + } + }, + computed: { + isAdminOrUp() { + return this.$store.getters['user/getIsAdminOrUp'] + } + }, + methods: { + shareYearInReviewServer() { + this.$refs.yearInReviewServer.share() + }, + shareYearInReview() { + this.$refs.yearInReview.share() + }, + refreshYearInReviewServer() { + this.$refs.yearInReviewServer.refresh() + }, + refreshYearInReview() { + this.$refs.yearInReview.refresh() + this.$refs.yearInReviewShort.refresh() + }, + clickShowYearInReview() { + this.showYearInReview = !this.showYearInReview + } + }, + beforeMount() { + this.yearInReviewYear = new Date().getFullYear() + // When not December show previous year + if (new Date().getMonth() < 11) { + this.yearInReviewYear-- + } + }, + mounted() { + if (typeof navigator.share !== 'undefined' && navigator.share) { + this.showShareButton = true + } else { + console.warn('Navigator.share not supported') + } + } +} +</script> \ No newline at end of file diff --git a/client/components/stats/YearInReviewServer.vue b/client/components/stats/YearInReviewServer.vue index 0d1fa8aa..3aeddfaf 100644 --- a/client/components/stats/YearInReviewServer.vue +++ b/client/components/stats/YearInReviewServer.vue @@ -1,24 +1,34 @@ <template> <div> - <div v-if="processing" class="w-[400px] h-[400px] flex items-center justify-center"> + <div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center"> <widgets-loading-spinner /> </div> - <img v-else-if="dataUrl" :src="dataUrl" /> + <img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" /> </div> </template> <script> export default { props: { - processing: Boolean + variant: { + type: Number, + default: 0 + }, + processing: Boolean, + year: Number }, data() { return { + canvas: null, dataUrl: null, - year: null, yearStats: null } }, + watch: { + variant() { + this.init() + } + }, methods: { async initCanvas() { if (!this.yearStats) return @@ -61,12 +71,6 @@ export default { ctx.fillText(text, x, y) } - const addIcon = (icon, color, fontSize, x, y) => { - ctx.fillStyle = color - ctx.font = `${fontSize} Material Icons Outlined` - ctx.fillText(icon, x, y) - } - // Bg color ctx.fillStyle = '#232323' ctx.fillRect(0, 0, canvas.width, canvas.height) @@ -168,28 +172,81 @@ export default { addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470) } - // Bottom images - imgsToAdd = Object.values(imgsToAdd) - if (imgsToAdd.length >= 5) { - addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540) + if (!this.variant) { + // Bottom images + imgsToAdd = Object.values(imgsToAdd) + if (imgsToAdd.length > 0) { + addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540) - for (let i = 0; i < 5; i++) { - let imgToAdd = imgsToAdd[i] - ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 580, 140, 140) + for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) { + let imgToAdd = imgsToAdd[i] + ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 580, 140, 140) + } + } + } else if (this.variant === 1) { + // Text stats + ctx.textAlign = 'left' + if (this.yearStats.topAuthors.length) { + addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549) + for (let i = 0; i < this.yearStats.topAuthors.length; i++) { + addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330) + } + } + + if (this.yearStats.topNarrators.length) { + addText('TOP NARRATORS', '24px', 'normal', tanColor, '1px', 430, 549) + for (let i = 0; i < this.yearStats.topNarrators.length; i++) { + addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330) + } + } + } else if (this.variant === 2) { + // Text stats + ctx.textAlign = 'left' + if (this.yearStats.topAuthors.length) { + addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549) + for (let i = 0; i < this.yearStats.topAuthors.length; i++) { + addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330) + } + } + + if (this.yearStats.topGenres.length) { + addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 549) + for (let i = 0; i < this.yearStats.topGenres.length; i++) { + addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330) + } } } + this.canvas = canvas this.dataUrl = canvas.toDataURL('png') }, + share() { + this.canvas.toBlob((blob) => { + const file = new File([blob], 'yearinreviewserver.png', { type: blob.type }) + const shareData = { + files: [file] + } + if (navigator.canShare(shareData)) { + navigator + .share(shareData) + .then(() => { + console.log('Share success') + }) + .catch((error) => { + console.error('Failed to share', error) + this.$toast.error('Failed to share: ' + error.message) + }) + } else { + this.$toast.error('Cannot share natively on this device') + } + }) + }, refresh() { this.init() }, async init() { this.$emit('update:processing', true) - let year = new Date().getFullYear() - if (new Date().getMonth() < 11) year-- - this.year = year - this.yearStats = await this.$axios.$get(`/api/stats/year/${year}`).catch((err) => { + this.yearStats = await this.$axios.$get(`/api/stats/year/${this.year}`).catch((err) => { console.error('Failed to load stats for year', err) this.$toast.error('Failed to load year stats') return null diff --git a/client/components/stats/YearInReviewShort.vue b/client/components/stats/YearInReviewShort.vue new file mode 100644 index 00000000..49abe122 --- /dev/null +++ b/client/components/stats/YearInReviewShort.vue @@ -0,0 +1,169 @@ +<template> + <div> + <div v-if="processing" class="max-w-[600px] h-32 sm:h-[200px] flex items-center justify-center"> + <widgets-loading-spinner /> + </div> + <img v-else-if="dataUrl" :src="dataUrl" /> + </div> +</template> + +<script> +export default { + props: { + processing: Boolean, + year: Number + }, + data() { + return { + dataUrl: null, + yearStats: null + } + }, + methods: { + async initCanvas() { + if (!this.yearStats) return + + const canvas = document.createElement('canvas') + canvas.width = 600 + canvas.height = 200 + const ctx = canvas.getContext('2d') + + const createRoundedRect = (x, y, w, h) => { + const grd1 = ctx.createLinearGradient(x, y, x + w, y + h) + grd1.addColorStop(0, '#44444455') + grd1.addColorStop(1, '#ffffff11') + ctx.fillStyle = grd1 + ctx.strokeStyle = '#C0C0C088' + ctx.beginPath() + ctx.roundRect(x, y, w, h, [20]) + ctx.fill() + ctx.stroke() + } + + const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => { + ctx.fillStyle = color + ctx.font = `${fontWeight} ${fontSize} Source Sans Pro` + ctx.letterSpacing = letterSpacing + + // If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis + if (maxWidth) { + let txtWidth = ctx.measureText(text).width + while (txtWidth > maxWidth) { + console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`) + if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time + else text = text.slice(0, -3) // First check remove last 3 chars + text += '...' + txtWidth = ctx.measureText(text).width + console.log(`Checking text "${text}" (width:${txtWidth})`) + } + } + + ctx.fillText(text, x, y) + } + + const addIcon = (icon, color, fontSize, x, y) => { + ctx.fillStyle = color + ctx.font = `${fontSize} Material Icons Outlined` + ctx.fillText(icon, x, y) + } + + // Bg color + ctx.fillStyle = '#232323' + ctx.fillRect(0, 0, canvas.width, canvas.height) + + // Cover image tiles + const bookCovers = this.yearStats.finishedBooksWithCovers + bookCovers.push(...this.yearStats.booksWithCovers) + + if (bookCovers.length) { + let index = 0 + ctx.globalAlpha = 0.25 + ctx.save() + ctx.translate(canvas.width / 2, canvas.height / 2) + ctx.rotate((-Math.PI / 180) * 25) + ctx.translate(-canvas.width / 2, -canvas.height / 2) + ctx.translate(-10, -90) + for (let x = 0; x < 4; x++) { + for (let y = 0; y < 3; y++) { + const coverIndex = index % bookCovers.length + let libraryItemId = bookCovers[coverIndex] + index++ + + await new Promise((resolve) => { + const img = new Image() + img.crossOrigin = 'anonymous' + img.addEventListener('load', () => { + let sw = img.width + if (img.width > img.height) { + sw = img.height + } + let sx = -(sw - img.width) / 2 + let sy = -(sw - img.height) / 2 + ctx.drawImage(img, sx, sy, sw, sw, 155 * x, 155 * y, 155, 155) + resolve() + }) + img.addEventListener('error', () => { + resolve() + }) + img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId) + }) + } + } + ctx.restore() + } + + ctx.globalAlpha = 1 + ctx.textBaseline = 'middle' + + // Create gradient + const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height) + grd1.addColorStop(0, '#000000aa') + grd1.addColorStop(1, '#cd9d49aa') + ctx.fillStyle = grd1 + ctx.fillRect(0, 0, canvas.width, canvas.height) + + // Top Abs icon + let tanColor = '#ffdb70' + ctx.fillStyle = tanColor + ctx.font = '42px absicons' + ctx.fillText('\ue900', 15, 36) + + // Top text + addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28) + addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51) + + // Top left box + createRoundedRect(15, 75, 280, 110) + addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120) + addText('books finished', '20px', 'normal', tanColor, '0px', 105, 155) + const readIconPath = new Path2D() + readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.5, d: 1.5, e: 55, f: 115 }) + ctx.fillStyle = '#ffffff' + ctx.fill(readIconPath) + + createRoundedRect(305, 75, 280, 110) + addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120) + addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155) + addIcon('local_library', 'white', '42px', 345, 130) + + this.dataUrl = canvas.toDataURL('png') + }, + refresh() { + this.init() + }, + async init() { + this.$emit('update:processing', true) + this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => { + console.error('Failed to load stats for year', err) + this.$toast.error('Failed to load year stats') + return null + }) + await this.initCanvas() + this.$emit('update:processing', false) + } + }, + mounted() { + this.init() + } +} +</script> \ No newline at end of file diff --git a/client/pages/config/stats.vue b/client/pages/config/stats.vue index 96581714..05d0550f 100644 --- a/client/pages/config/stats.vue +++ b/client/pages/config/stats.vue @@ -1,6 +1,9 @@ <template> <div> - <app-settings-content :header-text="$strings.HeaderYourStats"> + <!-- Year in review banner shown at the top in December and January --> + <stats-year-in-review-banner v-if="showYearInReviewBanner" /> + + <app-settings-content :header-text="$strings.HeaderYourStats" class="!mb-4"> <div class="flex justify-center"> <div class="flex p-2"> <svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24"> @@ -62,16 +65,10 @@ </div> </div> <stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" /> - - <ui-btn small :loading="processingYearInReview || processingYearInReviewAlt" @click.stop="clickShowYearInReview">{{ showYearInReview ? 'Refresh Year in Review' : 'Year in Review' }}</ui-btn> - <div v-if="showYearInReview"> - <div class="w-full h-px bg-slate-200/10 my-4" /> - - <stats-year-in-review ref="yearInReview" :processing.sync="processingYearInReview" /> - - <stats-year-in-review-server v-if="isAdminOrUp" ref="yearInReviewAlt" :processing.sync="processingYearInReviewAlt" /> - </div> </app-settings-content> + + <!-- Year in review banner shown at the bottom Feb - Nov --> + <stats-year-in-review-banner v-if="!showYearInReviewBanner" /> </div> </template> @@ -81,9 +78,7 @@ export default { return { listeningStats: null, windowWidth: 0, - showYearInReview: false, - processingYearInReview: false, - processingYearInReviewAlt: false + showYearInReviewBanner: false } }, watch: { @@ -126,22 +121,17 @@ export default { } }, methods: { - clickShowYearInReview() { - if (this.showYearInReview) { - this.$refs.yearInReview.refresh() - - if (this.$refs.yearInReviewAlt) { - this.$refs.yearInReviewAlt.refresh() - } - } else { - this.showYearInReview = true - } - }, async init() { this.listeningStats = await this.$axios.$get(`/api/me/listening-stats`).catch((err) => { console.error('Failed to load listening sesions', err) return [] }) + + let month = new Date().getMonth() + // January and December show year in review banner + if (month === 11 || month === 0) { + this.showYearInReviewBanner = true + } } }, mounted() { diff --git a/server/utils/queries/adminStats.js b/server/utils/queries/adminStats.js index 66a31e8d..f1d64f47 100644 --- a/server/utils/queries/adminStats.js +++ b/server/utils/queries/adminStats.js @@ -88,12 +88,53 @@ module.exports = { const numAuthorsAdded = await this.getNumAuthorsAddedForYear(year) + let authorListeningMap = {} + let narratorListeningMap = {} + let genreListeningMap = {} + const listeningSessions = await this.getListeningSessionsForYear(year) let totalListeningTime = 0 - for (const listeningSession of listeningSessions) { - totalListeningTime += (listeningSession.timeListening || 0) + for (const ls of listeningSessions) { + totalListeningTime += (ls.timeListening || 0) + + const authors = ls.mediaMetadata.authors || [] + authors.forEach((au) => { + if (!authorListeningMap[au.name]) authorListeningMap[au.name] = 0 + authorListeningMap[au.name] += (ls.timeListening || 0) + }) + + const narrators = ls.mediaMetadata.narrators || [] + narrators.forEach((narrator) => { + if (!narratorListeningMap[narrator]) narratorListeningMap[narrator] = 0 + narratorListeningMap[narrator] += (ls.timeListening || 0) + }) + + // Filter out bad genres like "audiobook" and "audio book" + const genres = (ls.mediaMetadata.genres || []).filter(g => !g.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book')) + genres.forEach((genre) => { + if (!genreListeningMap[genre]) genreListeningMap[genre] = 0 + genreListeningMap[genre] += (ls.timeListening || 0) + }) } + let topAuthors = null + topAuthors = Object.keys(authorListeningMap).map(authorName => ({ + name: authorName, + time: Math.round(authorListeningMap[authorName]) + })).sort((a, b) => b.time - a.time).slice(0, 3) + + let topNarrators = null + topNarrators = Object.keys(narratorListeningMap).map(narratorName => ({ + name: narratorName, + time: Math.round(narratorListeningMap[narratorName]) + })).sort((a, b) => b.time - a.time).slice(0, 3) + + let topGenres = null + topGenres = Object.keys(genreListeningMap).map(genre => ({ + genre, + time: Math.round(genreListeningMap[genre]) + })).sort((a, b) => b.time - a.time).slice(0, 3) + // Stats for total books, size and duration for everything added this year or earlier const [totalStatResultsRow] = await Database.sequelize.query(`SELECT SUM(li.size) AS totalSize, SUM(b.duration) AS totalDuration, COUNT(*) AS totalItems FROM libraryItems li, books b WHERE b.id = li.mediaId AND li.mediaType = 'book' AND li.createdAt < ":nextYear-01-01";`, { replacements: { @@ -112,7 +153,10 @@ module.exports = { totalBooksSize: totalStatResults?.totalSize || 0, totalBooksDuration: totalStatResults?.totalDuration || 0, totalListeningTime, - numBooks: totalStatResults?.totalItems || 0 + numBooks: totalStatResults?.totalItems || 0, + topAuthors, + topNarrators, + topGenres } } } diff --git a/server/utils/queries/userStats.js b/server/utils/queries/userStats.js index 0f997789..6fd5d506 100644 --- a/server/utils/queries/userStats.js +++ b/server/utils/queries/userStats.js @@ -52,12 +52,14 @@ module.exports = { }, include: { model: Database.bookModel, + attributes: ['id', 'title', 'coverPath'], include: { model: Database.libraryItemModel, attributes: ['id', 'mediaId', 'mediaType'] }, required: true - } + }, + order: Database.sequelize.random() }) return progresses }, @@ -69,6 +71,7 @@ module.exports = { async getStatsForYear(user, year) { const userId = user.id const listeningSessions = await this.getUserListeningSessionsForYear(userId, year) + const bookProgressesFinished = await this.getBookMediaProgressFinishedForYear(userId, year) let totalBookListeningTime = 0 let totalPodcastListeningTime = 0 @@ -79,11 +82,33 @@ module.exports = { let narratorListeningMap = {} let monthListeningMap = {} let bookListeningMap = {} - const booksWithCovers = [] + const booksWithCovers = [] + const finishedBooksWithCovers = [] + + // Get finished book stats + const numBooksFinished = bookProgressesFinished.length + let longestAudiobookFinished = null + for (const mediaProgress of bookProgressesFinished) { + // Grab first 5 that have a cover + if (mediaProgress.mediaItem?.coverPath && !finishedBooksWithCovers.includes(mediaProgress.mediaItem.libraryItem.id) && finishedBooksWithCovers.length < 5 && await fsExtra.pathExists(mediaProgress.mediaItem.coverPath)) { + finishedBooksWithCovers.push(mediaProgress.mediaItem.libraryItem.id) + } + + if (mediaProgress.duration && (!longestAudiobookFinished?.duration || mediaProgress.duration > longestAudiobookFinished.duration)) { + longestAudiobookFinished = { + id: mediaProgress.mediaItem.id, + title: mediaProgress.mediaItem.title, + duration: Math.round(mediaProgress.duration), + finishedAt: mediaProgress.finishedAt + } + } + } + + // Get listening session stats for (const ls of listeningSessions) { // Grab first 25 that have a cover - if (ls.mediaItem?.coverPath && !booksWithCovers.includes(ls.mediaItem.libraryItem.id) && booksWithCovers.length < 25 && await fsExtra.pathExists(ls.mediaItem.coverPath)) { + if (ls.mediaItem?.coverPath && !booksWithCovers.includes(ls.mediaItem.libraryItem.id) && !finishedBooksWithCovers.includes(ls.mediaItem.libraryItem.id) && booksWithCovers.length < 25 && await fsExtra.pathExists(ls.mediaItem.coverPath)) { booksWithCovers.push(ls.mediaItem.libraryItem.id) } @@ -162,21 +187,6 @@ module.exports = { } } - const bookProgressesFinished = await this.getBookMediaProgressFinishedForYear(userId, year) - - const numBooksFinished = bookProgressesFinished.length - let longestAudiobookFinished = null - bookProgressesFinished.forEach((mediaProgress) => { - if (mediaProgress.duration && (!longestAudiobookFinished?.duration || mediaProgress.duration > longestAudiobookFinished.duration)) { - longestAudiobookFinished = { - id: mediaProgress.mediaItem.id, - title: mediaProgress.mediaItem.title, - duration: Math.round(mediaProgress.duration), - finishedAt: mediaProgress.finishedAt - } - } - }) - return { totalListeningSessions: listeningSessions.length, totalListeningTime, @@ -189,7 +199,8 @@ module.exports = { numBooksFinished, numBooksListened: Object.keys(bookListeningMap).length, longestAudiobookFinished, - booksWithCovers + booksWithCovers, + finishedBooksWithCovers } } } From 9f366863a94ab4c241b6f650db9e6c00b4cb0a45 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 23 Dec 2023 16:16:24 -0600 Subject: [PATCH 0260/2145] Update comic reader buttons for mobile screens, add left scrollBy --- client/components/readers/ComicReader.vue | 30 +++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/client/components/readers/ComicReader.vue b/client/components/readers/ComicReader.vue index 9b09f5b6..67aa16c6 100644 --- a/client/components/readers/ComicReader.vue +++ b/client/components/readers/ComicReader.vue @@ -14,19 +14,20 @@ </div> </div> - <a v-if="pages && numPages" :href="mainImg" :download="pages[page - 1]" class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" :class="comicMetadata ? 'left-32' : 'left-20'"> - <span class="material-icons text-xl">download</span> - </a> - <div v-if="comicMetadata" class="absolute top-0 left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu"> - <span class="material-icons text-xl">more</span> - </div> - <div v-if="numPages" class="absolute top-0 left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowPageMenu"> + <div v-if="numPages" class="absolute top-0 left-4 sm:left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowPageMenu"> <span class="material-icons text-xl">menu</span> </div> - <div v-if="numPages" class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20"> + <div v-if="comicMetadata" class="absolute top-0 left-16 sm:left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu"> + <span class="material-icons text-xl">more</span> + </div> + <a v-if="pages && numPages" :href="mainImg" :download="pages[page - 1]" class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" :class="comicMetadata ? 'left-28 sm:left-32' : 'left-16 sm:left-20'"> + <span class="material-icons text-xl">download</span> + </a> + + <div v-if="numPages" class="absolute top-0 right-14 sm:right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20"> <p class="font-mono">{{ page }} / {{ numPages }}</p> </div> - <div v-if="mainImg" class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20"> + <div v-if="mainImg" class="absolute top-0 right-36 sm:right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20"> <ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" /> <ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" /> </div> @@ -91,7 +92,7 @@ export default { loadTimeout: null, loadedFirstPage: false, comicMetadata: null, - scale: 80, + scale: 80 } }, watch: { @@ -357,20 +358,19 @@ export default { scroll(event) { const imageContainer = this.$refs.imageContainer - console.log("Scrolling by " + event.deltaY) imageContainer.scrollBy({ top: event.deltaY, - behavior: "auto", - }); + left: event.deltaX, + behavior: 'auto' + }) } }, mounted() { const prevButton = this.$refs.prevButton const nextButton = this.$refs.nextButton - + prevButton.addEventListener('wheel', this.scroll, { passive: false }) nextButton.addEventListener('wheel', this.scroll, { passive: false }) - }, beforeDestroy() { const prevButton = this.$refs.prevButton From 5633113f256b5e9d70c6b32e7e61faf245616369 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 23 Dec 2023 16:39:56 -0600 Subject: [PATCH 0261/2145] Update share buttons to not show an error on abort --- client/components/stats/YearInReview.vue | 10 +++++--- .../components/stats/YearInReviewBanner.vue | 17 +++++++++---- .../components/stats/YearInReviewServer.vue | 10 +++++--- client/components/stats/YearInReviewShort.vue | 25 +++++++++++++++++++ 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/client/components/stats/YearInReview.vue b/client/components/stats/YearInReview.vue index 3fef3a8c..a6bed203 100644 --- a/client/components/stats/YearInReview.vue +++ b/client/components/stats/YearInReview.vue @@ -40,10 +40,10 @@ export default { const createRoundedRect = (x, y, w, h) => { const grd1 = ctx.createLinearGradient(x, y, x + w, y + h) - grd1.addColorStop(0, '#44444466') - grd1.addColorStop(1, '#ffffff22') + grd1.addColorStop(0, '#44444455') + grd1.addColorStop(1, '#ffffff11') ctx.fillStyle = grd1 - ctx.strokeStyle = '#C0C0C0aa' + ctx.strokeStyle = '#C0C0C088' ctx.beginPath() ctx.roundRect(x, y, w, h, [20]) ctx.fill() @@ -258,7 +258,9 @@ export default { }) .catch((error) => { console.error('Failed to share', error) - this.$toast.error('Failed to share: ' + error.message) + if (error.name !== 'AbortError') { + this.$toast.error('Failed to share: ' + error.message) + } }) } else { this.$toast.error('Cannot share natively on this device') diff --git a/client/components/stats/YearInReviewBanner.vue b/client/components/stats/YearInReviewBanner.vue index 9a0a0bbf..f7736078 100644 --- a/client/components/stats/YearInReviewBanner.vue +++ b/client/components/stats/YearInReviewBanner.vue @@ -1,5 +1,5 @@ <template> - <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-4"> + <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-1 sm:p-4 mb-4"> <!-- hack to get icon fonts loaded on init --> <div class="h-0 w-0 overflow-hidden opacity-0"> <span class="material-icons-outlined">close</span> @@ -26,7 +26,8 @@ <ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview"> Share </ui-btn> <div class="flex-grow" /> - <p class="text-lg font-semibold">Your Year <span class="hidden md:inline-block">in Review </span>({{ yearInReviewVariant + 1 }})</p> + <p class="hidden sm:block text-lg font-semibold">Your Year in Review ({{ yearInReviewVariant + 1 }})</p> + <p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p> <div class="flex-grow" /> <!-- refresh button --> @@ -44,6 +45,8 @@ <!-- your year in review short --> <div class="w-full max-w-[800px] mx-auto my-4"> + <!-- share button --> + <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort"> Share </ui-btn> <stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" /> </div> @@ -52,14 +55,15 @@ <div class="flex items-center justify-center mb-2"> <!-- previous button --> <ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--"> - <span class="material-icons-outlined text-lg px-1 sm:pr-1 py-px sm:py-0">chevron_left</span> + <span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> <span class="hidden sm:inline-block pr-2">Previous</span> </ui-btn> <!-- share button --> <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer"> Share </ui-btn> <div class="flex-grow" /> - <p class="text-lg font-semibold">Server <span class="hidden md:inline-block">Year in Review </span>({{ yearInReviewServerVariant + 1 }})</p> + <p class="hidden sm:block text-lg font-semibold">Server Year in Review ({{ yearInReviewServerVariant + 1 }})</p> + <p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p> <div class="flex-grow" /> <!-- refresh button --> @@ -70,7 +74,7 @@ <!-- next button --> <ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++"> <span class="hidden sm:inline-block pl-2">Next</span> - <span class="material-icons-outlined text-lg px-1 sm:pl-1 py-px sm:py-0">chevron_right</span> + <span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> </ui-btn> </div> </div> @@ -105,6 +109,9 @@ export default { shareYearInReview() { this.$refs.yearInReview.share() }, + shareYearInReviewShort() { + this.$refs.yearInReviewShort.share() + }, refreshYearInReviewServer() { this.$refs.yearInReviewServer.refresh() }, diff --git a/client/components/stats/YearInReviewServer.vue b/client/components/stats/YearInReviewServer.vue index 3aeddfaf..e6c10fa2 100644 --- a/client/components/stats/YearInReviewServer.vue +++ b/client/components/stats/YearInReviewServer.vue @@ -40,10 +40,10 @@ export default { const createRoundedRect = (x, y, w, h) => { const grd1 = ctx.createLinearGradient(x, y, x + w, y + h) - grd1.addColorStop(0, '#44444466') - grd1.addColorStop(1, '#ffffff22') + grd1.addColorStop(0, '#44444455') + grd1.addColorStop(1, '#ffffff11') ctx.fillStyle = grd1 - ctx.strokeStyle = '#C0C0C0aa' + ctx.strokeStyle = '#C0C0C088' ctx.beginPath() ctx.roundRect(x, y, w, h, [20]) ctx.fill() @@ -234,7 +234,9 @@ export default { }) .catch((error) => { console.error('Failed to share', error) - this.$toast.error('Failed to share: ' + error.message) + if (error.name !== 'AbortError') { + this.$toast.error('Failed to share: ' + error.message) + } }) } else { this.$toast.error('Cannot share natively on this device') diff --git a/client/components/stats/YearInReviewShort.vue b/client/components/stats/YearInReviewShort.vue index 49abe122..daa566a6 100644 --- a/client/components/stats/YearInReviewShort.vue +++ b/client/components/stats/YearInReviewShort.vue @@ -15,6 +15,7 @@ export default { }, data() { return { + canvas: null, dataUrl: null, yearStats: null } @@ -146,8 +147,32 @@ export default { addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155) addIcon('local_library', 'white', '42px', 345, 130) + this.canvas = canvas this.dataUrl = canvas.toDataURL('png') }, + share() { + this.canvas.toBlob((blob) => { + const file = new File([blob], 'yearinreviewserver.png', { type: blob.type + 'cat' }) + const shareData = { + files: [file] + } + if (navigator.canShare(shareData)) { + navigator + .share(shareData) + .then(() => { + console.log('Share success') + }) + .catch((error) => { + console.error('Failed to share', error) + if (error.name !== 'AbortError') { + this.$toast.error('Failed to share: ' + error.message) + } + }) + } else { + this.$toast.error('Cannot share natively on this device') + } + }) + }, refresh() { this.init() }, From b376f89ce5bb6483cf74b287ac0d37a9c1929530 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 23 Dec 2023 17:05:44 -0600 Subject: [PATCH 0262/2145] Version bump v2.7.0 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 16adf9db..fb8be23f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.6.0", + "version": "2.7.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index a13b5815..e404e7d4 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.6.0", + "version": "2.7.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 9df54fdd..8bd0b115 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.6.0", + "version": "2.7.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 061e2a7f..33f483b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.6.0", + "version": "2.7.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From a2db81bf7d07a5b8c5f91cf5dcc4e4395c494fc4 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 23 Dec 2023 17:13:44 -0600 Subject: [PATCH 0263/2145] Fix share button for year in review short card --- client/components/stats/YearInReviewShort.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/stats/YearInReviewShort.vue b/client/components/stats/YearInReviewShort.vue index daa566a6..18b12087 100644 --- a/client/components/stats/YearInReviewShort.vue +++ b/client/components/stats/YearInReviewShort.vue @@ -152,7 +152,7 @@ export default { }, share() { this.canvas.toBlob((blob) => { - const file = new File([blob], 'yearinreviewserver.png', { type: blob.type + 'cat' }) + const file = new File([blob], 'yearinreviewshort.png', { type: blob.type }) const shareData = { files: [file] } From cd7c4baaafd46fe165e6c561bb6e881e2429cc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Barga=C5=84ski?= <a.barganski@gmail.com> Date: Fri, 22 Dec 2023 20:35:38 +0100 Subject: [PATCH 0264/2145] Add: OPF file supports multiple series as sequence of : calibre:series and calibre:series_index; including tests --- server/scanner/OpfFileScanner.js | 7 +- server/utils/parsers/parseOpfMetadata.js | 24 ++++-- .../utils/parsers/parseOpfMetadata.test.js | 85 +++++++++++++++++++ 3 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 test/server/utils/parsers/parseOpfMetadata.test.js diff --git a/server/scanner/OpfFileScanner.js b/server/scanner/OpfFileScanner.js index abc2540a..87c4f565 100644 --- a/server/scanner/OpfFileScanner.js +++ b/server/scanner/OpfFileScanner.js @@ -32,11 +32,8 @@ class OpfFileScanner { bookMetadata.narrators = opfMetadata.narrators } } else if (key === 'series') { - if (opfMetadata.series) { - bookMetadata.series = [{ - name: opfMetadata.series, - sequence: opfMetadata.sequence || null - }] + if (opfMetadata.series?.length) { + bookMetadata.series = opfMetadata.series } } else if (opfMetadata[key] && key !== 'sequence') { bookMetadata[key] = opfMetadata[key] diff --git a/server/utils/parsers/parseOpfMetadata.js b/server/utils/parsers/parseOpfMetadata.js index d5fb4651..4c057197 100644 --- a/server/utils/parsers/parseOpfMetadata.js +++ b/server/utils/parsers/parseOpfMetadata.js @@ -100,13 +100,20 @@ function fetchLanguage(metadata) { } function fetchSeries(metadataMeta) { - if (!metadataMeta) return null - return fetchTagString(metadataMeta, "calibre:series") -} - -function fetchVolumeNumber(metadataMeta) { - if (!metadataMeta) return null - return fetchTagString(metadataMeta, "calibre:series_index") + if (!metadataMeta) return [] + const result = [] + for (let i = 0; i < metadataMeta.length; i++) { + if (metadataMeta[i].$.name === "calibre:series") { + const name = metadataMeta[i].$.content + let sequence = null + if (i + 1 < metadataMeta.length && + metadataMeta[i + 1].$.name === "calibre:series_index" && metadataMeta[i + 1].$.content) { + sequence = metadataMeta[i + 1].$.content + } + result.push({ name, sequence }) + } + } + return result } function fetchNarrators(creators, metadata) { @@ -173,8 +180,7 @@ module.exports.parseOpfMetadataXML = async (xml) => { description: fetchDescription(metadata), genres: fetchGenres(metadata), language: fetchLanguage(metadata), - series: fetchSeries(metadata.meta), - sequence: fetchVolumeNumber(metadata.meta), + series: fetchSeries(metadataMeta), tags: fetchTags(metadata) } return data diff --git a/test/server/utils/parsers/parseOpfMetadata.test.js b/test/server/utils/parsers/parseOpfMetadata.test.js new file mode 100644 index 00000000..c0732273 --- /dev/null +++ b/test/server/utils/parsers/parseOpfMetadata.test.js @@ -0,0 +1,85 @@ +const chai = require('chai') +const expect = chai.expect +const { parseOpfMetadataXML } = require('../../../../server/utils/parsers/parseOpfMetadata') + + +describe('parseOpfMetadata - test series', async () => { + it('test one serie', async() => { + const opf = ` + <?xml version='1.0' encoding='UTF-8'?> + <package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid"> + <metadata> + <meta name="calibre:series" content="Serie"/> + <meta name="calibre:series_index" content="1"/> + </metadata> + </package> + ` + const parsedOpf = await parseOpfMetadataXML(opf) + expect(parsedOpf.series).to.deep.equal([{"name": "Serie","sequence": "1"}]) + }) + + it('test more then 1 serie - in correct order', async() => { + const opf = ` + <?xml version='1.0' encoding='UTF-8'?> + <package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid"> + <metadata> + <meta name="calibre:series" content="Serie 1"/> + <meta name="calibre:series_index" content="1"/> + <meta name="calibre:series" content="Serie 2"/> + <meta name="calibre:series_index" content="2"/> + <meta name="calibre:series" content="Serie 3"/> + <meta name="calibre:series_index" content="3"/> + </metadata> + </package> + ` + const parsedOpf = await parseOpfMetadataXML(opf) + expect(parsedOpf.series).to.deep.equal([ + {"name": "Serie 1","sequence": "1"}, + {"name": "Serie 2","sequence": "2"}, + {"name": "Serie 3","sequence": "3"}, + ]) + }) + + it('test messed order of series content and index', async() => { + const opf = ` + <?xml version='1.0' encoding='UTF-8'?> + <package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid"> + <metadata> + <meta name="calibre:series" content="Serie 1"/> + <meta name="calibre:series_index" content="1"/> + <meta name="calibre:series_index" content="2"/> + <meta name="calibre:series_index" content="3"/> + <meta name="calibre:series" content="Serie 3"/> + </metadata> + </package> + ` + const parsedOpf = await parseOpfMetadataXML(opf) + expect(parsedOpf.series).to.deep.equal([ + {"name": "Serie 1","sequence": "1"}, + {"name": "Serie 3","sequence": null}, + ]) + }) + + it('test different values of series content and index', async() => { + const opf = ` + <?xml version='1.0' encoding='UTF-8'?> + <package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid"> + <metadata> + <meta name="calibre:series" content="Serie 1"/> + <meta name="calibre:series_index"/> + <meta name="calibre:series" content="Serie 2"/> + <meta name="calibre:series_index" content="abc"/> + <meta name="calibre:series" content="Serie 3"/> + <meta name="calibre:series_index" content=""/> + </metadata> + </package> + ` + const parsedOpf = await parseOpfMetadataXML(opf) + // console.log(JSON.stringify(parsedOpf, null, 4)) + expect(parsedOpf.series).to.deep.equal([ + {"name": "Serie 1", "sequence": null}, + {"name": "Serie 2", "sequence": "abc"}, + {"name": "Serie 3", "sequence": null}, + ]) + }) +}) From 6de0465b869aadecc80366e3fcd4cbc8483eb318 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 24 Dec 2023 11:41:27 -0600 Subject: [PATCH 0265/2145] Update opf parser to ignore series with empty content and add tests --- server/utils/parsers/parseOpfMetadata.js | 9 +-- .../utils/parsers/parseOpfMetadata.test.js | 76 +++++++++++++------ 2 files changed, 56 insertions(+), 29 deletions(-) diff --git a/server/utils/parsers/parseOpfMetadata.js b/server/utils/parsers/parseOpfMetadata.js index 4c057197..b51ceea5 100644 --- a/server/utils/parsers/parseOpfMetadata.js +++ b/server/utils/parsers/parseOpfMetadata.js @@ -103,12 +103,11 @@ function fetchSeries(metadataMeta) { if (!metadataMeta) return [] const result = [] for (let i = 0; i < metadataMeta.length; i++) { - if (metadataMeta[i].$.name === "calibre:series") { - const name = metadataMeta[i].$.content + if (metadataMeta[i].$?.name === "calibre:series" && metadataMeta[i].$.content?.trim()) { + const name = metadataMeta[i].$.content.trim() let sequence = null - if (i + 1 < metadataMeta.length && - metadataMeta[i + 1].$.name === "calibre:series_index" && metadataMeta[i + 1].$.content) { - sequence = metadataMeta[i + 1].$.content + if (metadataMeta[i + 1]?.$?.name === "calibre:series_index" && metadataMeta[i + 1].$?.content?.trim()) { + sequence = metadataMeta[i + 1].$.content.trim() } result.push({ name, sequence }) } diff --git a/test/server/utils/parsers/parseOpfMetadata.test.js b/test/server/utils/parsers/parseOpfMetadata.test.js index c0732273..f1d5ce89 100644 --- a/test/server/utils/parsers/parseOpfMetadata.test.js +++ b/test/server/utils/parsers/parseOpfMetadata.test.js @@ -2,27 +2,26 @@ const chai = require('chai') const expect = chai.expect const { parseOpfMetadataXML } = require('../../../../server/utils/parsers/parseOpfMetadata') - -describe('parseOpfMetadata - test series', async () => { - it('test one serie', async() => { +describe('parseOpfMetadata - test series', async () => { + it('test one series', async () => { const opf = ` <?xml version='1.0' encoding='UTF-8'?> <package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid"> - <metadata> + <metadata> <meta name="calibre:series" content="Serie"/> <meta name="calibre:series_index" content="1"/> </metadata> - </package> + </package> ` const parsedOpf = await parseOpfMetadataXML(opf) - expect(parsedOpf.series).to.deep.equal([{"name": "Serie","sequence": "1"}]) + expect(parsedOpf.series).to.deep.equal([{ "name": "Serie", "sequence": "1" }]) }) - it('test more then 1 serie - in correct order', async() => { + it('test more then 1 series - in correct order', async () => { const opf = ` <?xml version='1.0' encoding='UTF-8'?> <package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid"> - <metadata> + <metadata> <meta name="calibre:series" content="Serie 1"/> <meta name="calibre:series_index" content="1"/> <meta name="calibre:series" content="Serie 2"/> @@ -30,41 +29,41 @@ describe('parseOpfMetadata - test series', async () => { <meta name="calibre:series" content="Serie 3"/> <meta name="calibre:series_index" content="3"/> </metadata> - </package> + </package> ` const parsedOpf = await parseOpfMetadataXML(opf) expect(parsedOpf.series).to.deep.equal([ - {"name": "Serie 1","sequence": "1"}, - {"name": "Serie 2","sequence": "2"}, - {"name": "Serie 3","sequence": "3"}, + { "name": "Serie 1", "sequence": "1" }, + { "name": "Serie 2", "sequence": "2" }, + { "name": "Serie 3", "sequence": "3" }, ]) }) - it('test messed order of series content and index', async() => { + it('test messed order of series content and index', async () => { const opf = ` <?xml version='1.0' encoding='UTF-8'?> <package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid"> - <metadata> + <metadata> <meta name="calibre:series" content="Serie 1"/> <meta name="calibre:series_index" content="1"/> <meta name="calibre:series_index" content="2"/> <meta name="calibre:series_index" content="3"/> <meta name="calibre:series" content="Serie 3"/> </metadata> - </package> + </package> ` const parsedOpf = await parseOpfMetadataXML(opf) expect(parsedOpf.series).to.deep.equal([ - {"name": "Serie 1","sequence": "1"}, - {"name": "Serie 3","sequence": null}, + { "name": "Serie 1", "sequence": "1" }, + { "name": "Serie 3", "sequence": null }, ]) }) - it('test different values of series content and index', async() => { + it('test different values of series content and index', async () => { const opf = ` <?xml version='1.0' encoding='UTF-8'?> <package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid"> - <metadata> + <metadata> <meta name="calibre:series" content="Serie 1"/> <meta name="calibre:series_index"/> <meta name="calibre:series" content="Serie 2"/> @@ -72,14 +71,43 @@ describe('parseOpfMetadata - test series', async () => { <meta name="calibre:series" content="Serie 3"/> <meta name="calibre:series_index" content=""/> </metadata> - </package> + </package> ` const parsedOpf = await parseOpfMetadataXML(opf) - // console.log(JSON.stringify(parsedOpf, null, 4)) expect(parsedOpf.series).to.deep.equal([ - {"name": "Serie 1", "sequence": null}, - {"name": "Serie 2", "sequence": "abc"}, - {"name": "Serie 3", "sequence": null}, + { "name": "Serie 1", "sequence": null }, + { "name": "Serie 2", "sequence": "abc" }, + { "name": "Serie 3", "sequence": null }, + ]) + }) + + it('test empty series content', async () => { + const opf = ` + <?xml version='1.0' encoding='UTF-8'?> + <package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid"> + <metadata> + <meta name="calibre:series" content=""/> + <meta name="calibre:series_index" content=""/> + </metadata> + </package> + ` + const parsedOpf = await parseOpfMetadataXML(opf) + expect(parsedOpf.series).to.deep.equal([]) + }) + + it('test series and index using an xml namespace', async () => { + const opf = ` + <?xml version='1.0' encoding='UTF-8'?> + <ns0:package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid"> + <ns0:metadata> + <ns0:meta name="calibre:series" content="Serie 1"/> + <ns0:meta name="calibre:series_index" content=""/> + </ns0:metadata> + </ns0:package> + ` + const parsedOpf = await parseOpfMetadataXML(opf) + expect(parsedOpf.series).to.deep.equal([ + { "name": "Serie 1", "sequence": null } ]) }) }) From 14f42e15d1ef002e3504ab6509e0e90f704b1dbe Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 24 Dec 2023 11:53:57 -0600 Subject: [PATCH 0266/2145] Fix:Book scanner update book series sequence if changed --- server/scanner/BookScanner.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 48e8529a..6c93dddf 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -217,7 +217,8 @@ class BookScanner { } else if (key === 'series') { // Check for series added for (const seriesObj of bookMetadata.series) { - if (!media.series.some(se => se.name === seriesObj.name)) { + const existingBookSeries = media.series.find(se => se.name === seriesObj.name) + if (!existingBookSeries) { const existingSeries = Database.libraryFilterData[libraryItemData.libraryId].series.find(se => se.name === seriesObj.name) if (existingSeries) { await Database.bookSeriesModel.create({ @@ -238,6 +239,11 @@ class BookScanner { libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added new series "${seriesObj.name}"${seriesObj.sequence ? ` with sequence "${seriesObj.sequence}"` : ''}`) seriesUpdated = true } + } else if (seriesObj.sequence && existingBookSeries.bookSeries.sequence !== seriesObj.sequence) { + libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" series "${seriesObj.name}" sequence "${existingBookSeries.bookSeries.sequence || ''}" => "${seriesObj.sequence}"`) + seriesUpdated = true + existingBookSeries.bookSeries.sequence = seriesObj.sequence + await existingBookSeries.bookSeries.save() } } // Check for series removed @@ -657,7 +663,7 @@ class BookScanner { if (!this.libraryItemData.metadataNfoLibraryFile) return await NfoFileScanner.scanBookNfoFile(this.libraryItemData.metadataNfoLibraryFile, this.bookMetadata) } - + /** * Description from desc.txt and narrator from reader.txt */ From 209847d98ad67118e567c767d4cd2b07c3d4427e Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Mon, 25 Dec 2023 09:25:04 +0200 Subject: [PATCH 0267/2145] Add a SIGINT handler for proper server shutdown --- server/Server.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/Server.js b/server/Server.js index 5e8cab76..3de8ff7f 100644 --- a/server/Server.js +++ b/server/Server.js @@ -276,6 +276,19 @@ class Server { }) app.get('/healthcheck', (req, res) => res.sendStatus(200)) + let sigintAlreadyReceived = false + process.on('SIGINT', async () => { + if (!sigintAlreadyReceived) { + sigintAlreadyReceived = true + Logger.info('SIGINT (Ctrl+C) received. Shutting down...') + await this.stop() + Logger.info('Server stopped. Exiting.') + } else { + Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.') + } + process.exit(0) + }) + this.server.listen(this.Port, this.Host, () => { if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`) else Logger.info(`Listening on port :${this.Port}`) @@ -383,6 +396,7 @@ class Server { } async stop() { + Logger.info('=== Stopping Server ===') await this.watcher.close() Logger.info('Watcher Closed') From 0d0bdce3374108dfa21a1c6d3ff6469d9c6b63f3 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 25 Dec 2023 13:15:55 -0600 Subject: [PATCH 0268/2145] Fix:Fetch RSS feed request accept header #2446 --- server/utils/podcastUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 819ec914..4e01c92b 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -233,7 +233,7 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { method: 'GET', timeout: 12000, responseType: 'arraybuffer', - headers: { Accept: 'application/rss+xml' }, + headers: { Accept: 'application/rss+xml, application/xhtml+xml, application/xml' }, httpAgent: ssrfFilter(feedUrl), httpsAgent: ssrfFilter(feedUrl) }).then(async (data) => { From 21d0d43edc387dac8d4fc9a788a6548a80cda32a Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 27 Dec 2023 15:33:33 +0200 Subject: [PATCH 0269/2145] Add SocketAuthority.close() --- server/Server.js | 2 +- server/SocketAuthority.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/server/Server.js b/server/Server.js index 3de8ff7f..bb683826 100644 --- a/server/Server.js +++ b/server/Server.js @@ -401,7 +401,7 @@ class Server { Logger.info('Watcher Closed') return new Promise((resolve) => { - this.server.close((err) => { + SocketAuthority.close((err) => { if (err) { Logger.error('Failed to close server', err) } else { diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index da17f5df..b4698ef9 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -73,6 +73,15 @@ class SocketAuthority { } } + close(callback) { + Logger.info('[SocketAuthority] Shutting down') + // This will close all open socket connections, and also close the underlying http server + if (this.io) + this.io.close(callback) + else + callback() + } + initialize(Server) { this.Server = Server From 9a634e0de576d6e700f3a3a29f1394fc65347432 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 28 Dec 2023 16:32:21 -0600 Subject: [PATCH 0270/2145] Add JS docs for server stop --- server/Server.js | 6 +++++- server/SocketAuthority.js | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/server/Server.js b/server/Server.js index bb683826..57b9c74a 100644 --- a/server/Server.js +++ b/server/Server.js @@ -284,7 +284,7 @@ class Server { await this.stop() Logger.info('Server stopped. Exiting.') } else { - Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.') + Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.') } process.exit(0) }) @@ -395,6 +395,10 @@ class Server { res.sendStatus(200) } + /** + * Gracefully stop server + * Stops watcher and socket server + */ async stop() { Logger.info('=== Stopping Server ===') await this.watcher.close() diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index b4698ef9..00f0a63e 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -73,10 +73,15 @@ class SocketAuthority { } } + /** + * Closes the Socket.IO server and disconnect all clients + * + * @param {Function} callback + */ close(callback) { Logger.info('[SocketAuthority] Shutting down') // This will close all open socket connections, and also close the underlying http server - if (this.io) + if (this.io) this.io.close(callback) else callback() From e4effebc196226c1d4580964bbb3077bcaaab07a Mon Sep 17 00:00:00 2001 From: Jacob Southard <jacob@thevoltagesource.com> Date: Fri, 29 Dec 2023 10:04:59 -0600 Subject: [PATCH 0271/2145] Add try/catch to fileutils.getFileMtimeMs --- server/utils/fileUtils.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index ebad97db..7ef5320d 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -81,7 +81,12 @@ module.exports.getFileSize = async (path) => { * @returns {Promise<number>} epoch timestamp */ module.exports.getFileMTimeMs = async (path) => { - return (await getFileStat(path))?.mtimeMs || 0 + try { + return (await getFileStat(path))?.mtimeMs || 0 + } catch (err) { + Logger.error(`[fileUtils] Failed to getFileMtimeMs`, err) + return 0 + } } /** From 269676e8a5c3c2413c0ab80625c3a9c1ba8ce3aa Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 29 Dec 2023 17:05:35 -0600 Subject: [PATCH 0272/2145] Update:CORS for /cover API endpoint for use in canvas in the mobile apps --- server/Server.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/Server.js b/server/Server.js index 3de8ff7f..070ef36f 100644 --- a/server/Server.js +++ b/server/Server.js @@ -136,15 +136,16 @@ class Server { /** * @temporary - * This is necessary for the ebook API endpoint in the mobile apps + * This is necessary for the ebook & cover 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 + * The cover image is fetched with XMLHttpRequest in the mobile apps to load into a canvas and extract colors * @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 (Logger.isDev || 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|cover)(\/[0-9]+)?/)) { const allowedOrigins = ['capacitor://localhost', 'http://localhost'] if (Logger.isDev || allowedOrigins.some(o => o === req.get('origin'))) { res.header('Access-Control-Allow-Origin', req.get('origin')) @@ -284,7 +285,7 @@ class Server { await this.stop() Logger.info('Server stopped. Exiting.') } else { - Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.') + Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.') } process.exit(0) }) From 456bb87a008e1f19dc9ed16c2990babff851ab84 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 30 Dec 2023 12:12:48 -0600 Subject: [PATCH 0273/2145] Update:Find one library item endpoint sequelize query split into two queries to improve performance #2073 #2075 --- server/models/LibraryItem.js | 63 ++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index b6f2f285..ffd2f9c0 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -419,39 +419,40 @@ class LibraryItem extends Model { */ static async getOldById(libraryItemId) { if (!libraryItemId) return null - const libraryItem = await this.findByPk(libraryItemId, { - include: [ - { - model: this.sequelize.models.book, - include: [ - { - model: this.sequelize.models.author, - through: { - attributes: [] - } - }, - { - model: this.sequelize.models.series, - through: { - attributes: ['sequence'] - } + + const libraryItem = await this.findByPk(libraryItemId) + + if (libraryItem.mediaType === 'podcast') { + libraryItem.media = await libraryItem.getMedia({ + include: [ + { + model: this.sequelize.models.podcastEpisode + } + ] + }) + } else { + libraryItem.media = await libraryItem.getMedia({ + include: [ + { + model: this.sequelize.models.author, + through: { + attributes: [] } - ] - }, - { - model: this.sequelize.models.podcast, - include: [ - { - model: this.sequelize.models.podcastEpisode + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] } - ] - } - ], - order: [ - [this.sequelize.models.book, this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'], - [this.sequelize.models.book, this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'] - ] - }) + } + ], + order: [ + [this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'], + [this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'] + ] + }) + } + if (!libraryItem) return null return this.getOldLibraryItem(libraryItem) } From 160c83df4a0206f54cfa501c781032e5c8745cc6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 30 Dec 2023 16:14:14 -0600 Subject: [PATCH 0274/2145] Update:podcastEpisodes table index added for createdAt column #2073 #2075 --- server/controllers/LibraryItemController.js | 1 - server/models/LibraryItem.js | 6 +++++- server/models/PodcastEpisode.js | 7 ++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index b0ecf446..f462c081 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -49,7 +49,6 @@ class LibraryItemController { item.episodesDownloading = [this.podcastManager.currentDownload.toJSONForClient()] } } - return res.json(item) } res.json(req.libraryItem) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index ffd2f9c0..67e9abfb 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -421,6 +421,10 @@ class LibraryItem extends Model { if (!libraryItemId) return null const libraryItem = await this.findByPk(libraryItemId) + if (!libraryItem) { + Logger.error(`[LibraryItem] Library item not found with id "${libraryItemId}"`) + return null + } if (libraryItem.mediaType === 'podcast') { libraryItem.media = await libraryItem.getMedia({ @@ -453,7 +457,7 @@ class LibraryItem extends Model { }) } - if (!libraryItem) return null + if (!libraryItem.media) return null return this.getOldLibraryItem(libraryItem) } diff --git a/server/models/PodcastEpisode.js b/server/models/PodcastEpisode.js index 55b2f9d4..2fdefb86 100644 --- a/server/models/PodcastEpisode.js +++ b/server/models/PodcastEpisode.js @@ -152,7 +152,12 @@ class PodcastEpisode extends Model { extraData: DataTypes.JSON }, { sequelize, - modelName: 'podcastEpisode' + modelName: 'podcastEpisode', + indexes: [ + { + fields: ['createdAt'] + } + ] }) const { podcast } = sequelize.models From 021adf31043c2d3a0595652ac12aee785dea4360 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 31 Dec 2023 14:51:01 -0600 Subject: [PATCH 0275/2145] Update:Podcast episode table is lazy loaded #1549 --- .../components/tables/LibraryFilesTable.vue | 4 +- ...EpisodeTableRow.vue => LazyEpisodeRow.vue} | 177 ++++++++------- ...pisodesTable.vue => LazyEpisodesTable.vue} | 202 ++++++++++++++++-- client/pages/item/_id/index.vue | 4 +- server/controllers/LibraryItemController.js | 1 + server/objects/entities/PodcastEpisode.js | 17 +- 6 files changed, 300 insertions(+), 105 deletions(-) rename client/components/tables/podcast/{EpisodeTableRow.vue => LazyEpisodeRow.vue} (55%) rename client/components/tables/podcast/{EpisodesTable.vue => LazyEpisodesTable.vue} (66%) diff --git a/client/components/tables/LibraryFilesTable.vue b/client/components/tables/LibraryFilesTable.vue index fef1ae5a..4160c783 100644 --- a/client/components/tables/LibraryFilesTable.vue +++ b/client/components/tables/LibraryFilesTable.vue @@ -12,7 +12,7 @@ </div> </div> <transition name="slide"> - <div class="w-full" v-show="showFiles"> + <div class="w-full" v-if="showFiles"> <table class="text-sm tracksTable"> <tr> <th class="text-left px-4">{{ $strings.LabelPath }}</th> @@ -70,7 +70,7 @@ export default { }, audioFiles() { if (this.libraryItem.mediaType === 'podcast') { - return this.libraryItem.media?.episodes.map((ep) => ep.audioFile) || [] + return this.libraryItem.media?.episodes.map((ep) => ep.audioFile).filter((af) => af) || [] } return this.libraryItem.media?.audioFiles || [] }, diff --git a/client/components/tables/podcast/EpisodeTableRow.vue b/client/components/tables/podcast/LazyEpisodeRow.vue similarity index 55% rename from client/components/tables/podcast/EpisodeTableRow.vue rename to client/components/tables/podcast/LazyEpisodeRow.vue index 4300b8e1..d2b106fe 100644 --- a/client/components/tables/podcast/EpisodeTableRow.vue +++ b/client/components/tables/podcast/LazyEpisodeRow.vue @@ -1,18 +1,22 @@ <template> - <div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave"> - <div v-if="episode" class="flex items-center cursor-pointer" :class="{ 'opacity-70': isSelected || selectionMode }" @click="clickedEpisode"> - <div class="flex-grow px-2"> + <div :id="`lazy-episode-${index}`" class="w-full h-full cursor-pointer" @mouseover="mouseover" @mouseleave="mouseleave"> + <div class="flex" @click="clickedEpisode"> + <div class="flex-grow"> <div class="flex items-center"> - <span class="text-sm font-semibold">{{ title }}</span> - <widgets-podcast-type-indicator :type="episode.episodeType" /> + <span class="text-sm font-semibold">{{ episodeTitle }}</span> + <widgets-podcast-type-indicator :type="episodeType" /> </div> - <p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle"></p> - <div class="flex justify-between pt-2 max-w-xl"> - <p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p> - <p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p> - <p v-if="episode.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p> - <p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p> + <div class="h-10 flex items-center mt-1.5 mb-0.5"> + <p class="text-sm text-gray-200 episode-subtitle" v-html="episodeSubtitle"></p> + </div> + <div class="h-8 flex items-center"> + <div class="w-full inline-flex justify-between max-w-xl"> + <p v-if="episode?.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p> + <p v-if="episode?.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p> + <p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p> + <p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p> + </div> </div> <div class="flex items-center pt-2"> @@ -37,10 +41,11 @@ <ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" /> </div> </div> - <div v-if="isHovering || isSelected || selectionMode" class="hidden md:block w-12 min-w-12" /> + <div v-if="isHovering || isSelected || isSelectionMode" class="hidden md:block w-12 min-w-12" /> </div> - <div v-if="isSelected || selectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-10 z-10 cursor-pointer" @click.stop="clickedSelectionBg" /> - <div class="hidden md:block md:w-12 md:min-w-12 md:-right-0 md:absolute md:top-0 h-full transform transition-transform z-20" :class="!isHovering && !isSelected && !selectionMode ? 'translate-x-24' : 'translate-x-0'"> + + <div v-if="isSelected || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-10 z-10 cursor-pointer" @click.stop="clickedSelectionBg" /> + <div class="hidden md:block md:w-12 md:min-w-12 md:-right-0 md:absolute md:top-0 h-full transform transition-transform z-20" :class="!isHovering && !isSelected && !isSelectionMode ? 'translate-x-24' : 'translate-x-0'"> <div class="flex h-full items-center"> <div class="mx-1"> <ui-checkbox v-model="isSelected" @input="selectedUpdated" checkbox-bg="bg" /> @@ -55,84 +60,89 @@ <script> export default { props: { + index: Number, libraryItemId: String, episode: { type: Object, - default: () => {} - }, - selectionMode: Boolean + default: () => null + } }, data() { return { isProcessingReadUpdate: false, processingRemove: false, isHovering: false, - isSelected: false + isSelected: false, + isSelectionMode: false } }, computed: { + store() { + return this.$store || this.$nuxt.$store + }, + axios() { + return this.$axios || this.$nuxt.$axios + }, userCanUpdate() { - return this.$store.getters['user/getUserCanUpdate'] + return this.store.getters['user/getUserCanUpdate'] }, userCanDelete() { - return this.$store.getters['user/getUserCanDelete'] + return this.store.getters['user/getUserCanDelete'] }, - audioFile() { - return this.episode.audioFile + episodeId() { + return this.episode?.id || '' }, - title() { - return this.episode.title || '' + episodeTitle() { + return this.episode?.title || '' }, - subtitle() { - return this.episode.subtitle || this.description + episodeSubtitle() { + return this.episode?.subtitle || '' }, - description() { - return this.episode.description || '' + episodeType() { + return this.episode?.episodeType || '' }, - duration() { - return this.$secondsToTimestamp(this.episode.duration) + publishedAt() { + return this.episode?.publishedAt }, - libraryItemIdStreaming() { - return this.$store.getters['getLibraryItemIdStreaming'] - }, - isStreamingFromDifferentLibrary() { - return this.$store.getters['getIsStreamingFromDifferentLibrary'] - }, - isStreaming() { - return this.$store.getters['getIsMediaStreaming'](this.libraryItemId, this.episode.id) - }, - isQueued() { - return this.$store.getters['getIsMediaQueued'](this.libraryItemId, this.episode.id) - }, - streamIsPlaying() { - return this.$store.state.streamIsPlaying && this.isStreaming + dateFormat() { + return this.store.state.serverSettings.dateFormat }, itemProgress() { - return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id) + return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episodeId) }, itemProgressPercent() { - return this.itemProgress ? this.itemProgress.progress : 0 + return this.itemProgress?.progress || 0 }, userIsFinished() { - return this.itemProgress ? !!this.itemProgress.isFinished : false + return !!this.itemProgress?.isFinished + }, + libraryItemIdStreaming() { + return this.store.getters['getLibraryItemIdStreaming'] + }, + isStreamingFromDifferentLibrary() { + return this.store.getters['getIsStreamingFromDifferentLibrary'] + }, + isStreaming() { + return this.store.getters['getIsMediaStreaming'](this.libraryItemId, this.episodeId) + }, + isQueued() { + return this.store.getters['getIsMediaQueued'](this.libraryItemId, this.episodeId) + }, + streamIsPlaying() { + return this.store.state.streamIsPlaying && this.isStreaming }, timeRemaining() { if (this.streamIsPlaying) return 'Playing' - if (!this.itemProgress) return this.$elapsedPretty(this.episode.duration) + if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0) if (this.userIsFinished) return 'Finished' - var remaining = Math.floor(this.itemProgress.duration - this.itemProgress.currentTime) + const remaining = Math.floor(this.itemProgress.duration - this.itemProgress.currentTime) return `${this.$elapsedPretty(remaining)} left` - }, - publishedAt() { - return this.episode.publishedAt - }, - dateFormat() { - return this.$store.state.serverSettings.dateFormat } }, methods: { - clickAddToPlaylist() { - this.$emit('addToPlaylist', this.episode) + setSelectionMode(isSelectionMode) { + this.isSelectionMode = isSelectionMode + if (!this.isSelectionMode) this.isSelected = false }, clickedEpisode() { this.$emit('view', this.episode) @@ -150,16 +160,23 @@ export default { mouseleave() { this.isHovering = false }, - clickEdit() { - this.$emit('edit', this.episode) - }, playClick() { if (this.streamIsPlaying) { - this.$eventBus.$emit('pause-item') + const eventBus = this.$eventBus || this.$nuxt.$eventBus + eventBus.$emit('pause-item') } else { this.$emit('play', this.episode) } }, + queueBtnClick() { + if (this.isQueued) { + // Remove from queue + this.store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId, episodeId: this.episodeId }) + } else { + // Add to queue + this.$emit('addToQueue', this.episode) + } + }, toggleFinished(confirmed = false) { if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) { const payload = { @@ -171,37 +188,47 @@ export default { }, type: 'yesNo' } - this.$store.commit('globals/setConfirmPrompt', payload) + this.store.commit('globals/setConfirmPrompt', payload) return } - var updatePayload = { + const updatePayload = { isFinished: !this.userIsFinished } this.isProcessingReadUpdate = true - this.$axios - .$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload) + this.axios + .$patch(`/api/me/progress/${this.libraryItemId}/${this.episodeId}`, updatePayload) .then(() => { this.isProcessingReadUpdate = false }) .catch((error) => { console.error('Failed', error) this.isProcessingReadUpdate = false - this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed) + const toast = this.$toast || this.$nuxt.$toast + toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed) }) }, + clickAddToPlaylist() { + this.$emit('addToPlaylist', this.episode) + }, + clickEdit() { + this.$emit('edit', this.episode) + }, removeClick() { this.$emit('remove', this.episode) }, - queueBtnClick() { - if (this.isQueued) { - // Remove from queue - this.$store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId, episodeId: this.episode.id }) - } else { - // Add to queue - this.$emit('addToQueue', this.episode) + destroy() { + // destroy the vue listeners, etc + this.$destroy() + + // remove the element from the DOM + if (this.$el && this.$el.parentNode) { + this.$el.parentNode.removeChild(this.$el) + } else if (this.$el && this.$el.remove) { + this.$el.remove() } } - } + }, + mounted() {} } -</script> +</script> \ No newline at end of file diff --git a/client/components/tables/podcast/EpisodesTable.vue b/client/components/tables/podcast/LazyEpisodesTable.vue similarity index 66% rename from client/components/tables/podcast/EpisodesTable.vue rename to client/components/tables/podcast/LazyEpisodesTable.vue index 9534b34e..b1fb03ac 100644 --- a/client/components/tables/podcast/EpisodesTable.vue +++ b/client/components/tables/podcast/LazyEpisodesTable.vue @@ -1,5 +1,5 @@ <template> - <div class="w-full py-6"> + <div id="lazy-episodes-table" class="w-full py-6"> <div class="flex flex-wrap flex-col md:flex-row md:items-center mb-4"> <div class="flex items-center flex-nowrap whitespace-nowrap mb-2 md:mb-0"> <p class="text-lg mb-0 font-semibold">{{ $strings.HeaderEpisodes }}</p> @@ -18,28 +18,41 @@ <ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn> </template> <template v-else> - <controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 md:ml-4" /> - <controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" /> + <controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 md:ml-4" @change="filterSortChanged" /> + <controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" @change="filterSortChanged" /> <div class="flex-grow md:hidden" /> <ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" /> </template> </div> </div> - <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> + <!-- <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> --> <div v-if="episodes.length" class="w-full py-3 mx-auto flex"> <form @submit.prevent="submit" class="flex flex-grow"> <ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" /> </form> </div> - <template v-for="episode in episodesList"> - <tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" /> - </template> + <div class="relative min-h-[176px]"> + <template v-for="episode in totalEpisodes"> + <div :key="episode" :id="`episode-${episode - 1}`" class="w-full h-44 px-2 py-3 overflow-hidden relative border-b border-white/10"> + <!-- episode is mounted here --> + </div> + </template> + <div v-if="isSearching" class="w-full h-full absolute inset-0 flex justify-center py-12" :class="{ 'bg-black/50': totalEpisodes }"> + <ui-loading-indicator /> + </div> + <div v-else-if="!totalEpisodes" class="h-44 flex items-center justify-center"> + <p class="text-lg">{{ $strings.MessageNoEpisodes }}</p> + </div> + </div> <modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" /> </div> </template> <script> +import Vue from 'vue' +import LazyEpisodeRow from './LazyEpisodeRow.vue' + export default { props: { libraryItem: { @@ -60,7 +73,15 @@ export default { processing: false, search: null, searchTimeout: null, - searchText: null + searchText: null, + isSearching: false, + totalEpisodes: 0, + episodesPerPage: null, + episodeIndexesMounted: [], + episodeComponentRefs: {}, + windowHeight: 0, + episodesTableOffsetTop: 0, + episodeRowHeight: 176 } }, watch: { @@ -194,13 +215,19 @@ export default { submit() {}, inputUpdate() { clearTimeout(this.searchTimeout) + this.isSearching = true + let searchStart = this.searchText this.searchTimeout = setTimeout(() => { - if (!this.search || !this.search.trim()) { + this.isSearching = false + if (!this.search?.trim()) { this.searchText = '' - return + } else { + this.searchText = this.search.toLowerCase().trim() } - this.searchText = this.search.toLowerCase().trim() - }, 500) + if (searchStart !== this.searchText) { + this.init() + } + }, 750) }, contextMenuAction({ action }) { if (action === 'quick-match-episodes') { @@ -304,24 +331,30 @@ export default { if (!val) this.episodesToRemove = [] }, clearSelected() { - const episodeRows = this.$refs.episodeRow - if (episodeRows && episodeRows.length) { - for (const epRow of episodeRows) { - if (epRow) epRow.isSelected = false - } - } this.selectedEpisodes = [] + this.setSelectionModeForEpisodes() }, removeSelectedEpisodes() { this.episodesToRemove = this.selectedEpisodes this.showPodcastRemoveModal = true }, episodeSelected({ isSelected, episode }) { + let isSelectionModeBefore = this.isSelectionMode if (isSelected) { this.selectedEpisodes.push(episode) } else { this.selectedEpisodes = this.selectedEpisodes.filter((ep) => ep.id !== episode.id) } + if (this.isSelectionMode !== isSelectionModeBefore) { + this.setSelectionModeForEpisodes() + } + }, + setSelectionModeForEpisodes() { + for (const key in this.episodeComponentRefs) { + if (this.episodeComponentRefs[key]?.setSelectionMode) { + this.episodeComponentRefs[key].setSelectionMode(this.isSelectionMode) + } + } }, playEpisode(episode) { const queueItems = [] @@ -367,12 +400,143 @@ export default { this.$store.commit('globals/setSelectedEpisode', episode) this.$store.commit('globals/setShowViewPodcastEpisodeModal', true) }, + destroyEpisodeComponents() { + for (const key in this.episodeComponentRefs) { + if (this.episodeComponentRefs[key]?.destroy) { + this.episodeComponentRefs[key].destroy() + } + } + this.episodeComponentRefs = {} + this.episodeIndexesMounted = [] + }, + mountEpisode(index) { + const episodeEl = document.getElementById(`episode-${index}`) + if (!episodeEl) { + console.warn('Episode row el not found at ' + index) + return + } + + this.episodeIndexesMounted.push(index) + + if (this.episodeComponentRefs[index]) { + const episodeComponent = this.episodeComponentRefs[index] + episodeEl.appendChild(episodeComponent.$el) + if (this.isSelectionMode) { + episodeComponent.setSelectionMode(true) + if (this.selectedEpisodes.some((i) => i.id === episodeComponent.episodeId)) { + episodeComponent.isSelected = true + } else { + episodeComponent.isSelected = false + } + } else { + episodeComponent.setSelectionMode(false) + } + } else { + const _this = this + const ComponentClass = Vue.extend(LazyEpisodeRow) + const instance = new ComponentClass({ + propsData: { + index, + libraryItemId: this.libraryItem.id, + episode: this.episodesList[index] + }, + created() { + this.$on('selected', (payload) => { + _this.episodeSelected(payload) + }) + this.$on('view', (payload) => { + _this.viewEpisode(payload) + }) + this.$on('play', (payload) => { + _this.playEpisode(payload) + }) + this.$on('addToQueue', (payload) => { + _this.addEpisodeToQueue(payload) + }) + this.$on('remove', (payload) => { + _this.removeEpisode(payload) + }) + this.$on('edit', (payload) => { + _this.editEpisode(payload) + }) + this.$on('addToPlaylist', (payload) => { + _this.addToPlaylist(payload) + }) + } + }) + this.episodeComponentRefs[index] = instance + instance.$mount() + episodeEl.appendChild(instance.$el) + + if (this.isSelectionMode) { + instance.setSelectionMode(true) + if (this.selectedEpisodes.some((i) => i.id === this.episodesList[index].id)) { + instance.isSelected = true + } + } + } + }, + mountEpisodes(startIndex, endIndex) { + for (let i = startIndex; i < endIndex; i++) { + if (!this.episodeIndexesMounted.includes(i)) { + this.mountEpisode(i) + } + } + }, + scroll(evt) { + if (!evt?.target?.scrollTop) return + const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0) + let firstEpisodeIndex = Math.floor(scrollTop / this.episodeRowHeight) + let lastEpisodeIndex = Math.ceil((scrollTop + this.windowHeight) / this.episodeRowHeight) + lastEpisodeIndex = Math.min(this.totalEpisodes - 1, lastEpisodeIndex) + + this.episodeIndexesMounted = this.episodeIndexesMounted.filter((_index) => { + if (_index < firstEpisodeIndex || _index >= lastEpisodeIndex) { + const el = document.getElementById(`lazy-episode-${_index}`) + if (el) el.remove() + return false + } + return true + }) + this.mountEpisodes(firstEpisodeIndex, lastEpisodeIndex + 1) + }, + initListeners() { + const itemPageWrapper = document.getElementById('item-page-wrapper') + if (itemPageWrapper) { + itemPageWrapper.addEventListener('scroll', this.scroll) + } + }, + removeListeners() { + const itemPageWrapper = document.getElementById('item-page-wrapper') + if (itemPageWrapper) { + itemPageWrapper.removeEventListener('scroll', this.scroll) + } + }, + filterSortChanged() { + this.init() + }, init() { - this.episodesCopy = this.episodes.map((ep) => ({ ...ep })) + this.destroyEpisodeComponents() + this.totalEpisodes = this.episodesList.length + + const lazyEpisodesTableEl = document.getElementById('lazy-episodes-table') + this.episodesTableOffsetTop = (lazyEpisodesTableEl?.offsetTop || 0) + 64 + + this.windowHeight = window.innerHeight + this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight) + + this.$nextTick(() => { + this.mountEpisodes(0, Math.min(this.episodesPerPage, this.totalEpisodes)) + }) } }, mounted() { + this.episodesCopy = this.episodes.map((ep) => ({ ...ep })) + this.initListeners() this.init() + }, + beforeDestroy() { + this.removeListeners() } } </script> diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 8658a6e4..073ec570 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -1,6 +1,6 @@ <template> <div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamLibraryItem ? 'streaming' : ''"> - <div class="w-full h-full overflow-y-auto px-2 py-6 lg:p-8"> + <div id="item-page-wrapper" 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 group" style="height: fit-content"> @@ -136,7 +136,7 @@ <widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" /> - <tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" /> + <tables-podcast-lazy-episodes-table v-if="isPodcast" :library-item="libraryItem" /> <tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" /> diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index f462c081..b0ecf446 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -49,6 +49,7 @@ class LibraryItemController { item.episodesDownloading = [this.podcastManager.currentDownload.toJSONForClient()] } } + return res.json(item) } res.json(req.libraryItem) diff --git a/server/objects/entities/PodcastEpisode.js b/server/objects/entities/PodcastEpisode.js index 1452b7b5..cc8b8d9b 100644 --- a/server/objects/entities/PodcastEpisode.js +++ b/server/objects/entities/PodcastEpisode.js @@ -48,12 +48,14 @@ class PodcastEpisode { this.guid = episode.guid || null this.pubDate = episode.pubDate this.chapters = episode.chapters?.map(ch => ({ ...ch })) || [] - this.audioFile = new AudioFile(episode.audioFile) + this.audioFile = episode.audioFile ? new AudioFile(episode.audioFile) : null this.publishedAt = episode.publishedAt this.addedAt = episode.addedAt this.updatedAt = episode.updatedAt - this.audioFile.index = 1 // Only 1 audio file per episode + if (this.audioFile) { + this.audioFile.index = 1 // Only 1 audio file per episode + } } toJSON() { @@ -73,7 +75,7 @@ class PodcastEpisode { guid: this.guid, pubDate: this.pubDate, chapters: this.chapters.map(ch => ({ ...ch })), - audioFile: this.audioFile.toJSON(), + audioFile: this.audioFile?.toJSON() || null, publishedAt: this.publishedAt, addedAt: this.addedAt, updatedAt: this.updatedAt @@ -97,8 +99,8 @@ class PodcastEpisode { guid: this.guid, pubDate: this.pubDate, chapters: this.chapters.map(ch => ({ ...ch })), - audioFile: this.audioFile.toJSON(), - audioTrack: this.audioTrack.toJSON(), + audioFile: this.audioFile?.toJSON() || null, + audioTrack: this.audioTrack?.toJSON() || null, publishedAt: this.publishedAt, addedAt: this.addedAt, updatedAt: this.updatedAt, @@ -108,6 +110,7 @@ class PodcastEpisode { } get audioTrack() { + if (!this.audioFile) return null const audioTrack = new AudioTrack() audioTrack.setData(this.libraryItemId, this.audioFile, 0) return audioTrack @@ -116,9 +119,9 @@ class PodcastEpisode { return [this.audioTrack] } get duration() { - return this.audioFile.duration + return this.audioFile?.duration || 0 } - get size() { return this.audioFile.metadata.size } + get size() { return this.audioFile?.metadata.size || 0 } get enclosureUrl() { return this.enclosure?.url || null } From fececd4651eb1851008d98ed95457c4757d7b88b Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 31 Dec 2023 15:09:35 -0600 Subject: [PATCH 0276/2145] Fix:Playlists navigation button not showing on mobile screen #2469 --- client/components/app/BookShelfToolbar.vue | 7 +++++++ client/store/libraries.js | 11 +++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index ab33a5b3..bd31768b 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -22,6 +22,10 @@ <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> + <nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> + <p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p> + <span v-else class="material-icons-outlined text-lg">queue_music</span> + </nuxt-link> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p> <span v-else class="material-icons-outlined text-lg">collections_bookmark</span> @@ -293,6 +297,9 @@ export default { } return items + }, + showPlaylists() { + return this.$store.state.libraries.numUserPlaylists > 0 } }, methods: { diff --git a/client/store/libraries.js b/client/store/libraries.js index fd8af4ae..8771ebcf 100644 --- a/client/store/libraries.js +++ b/client/store/libraries.js @@ -80,13 +80,11 @@ export const actions = { return state.folders } } - console.log('Loading folders') commit('setFoldersLastUpdate') return this.$axios .$get('/api/filesystem') .then((res) => { - console.log('Settings folders', res) commit('setFolders', res.directories) return res.directories }) @@ -119,15 +117,16 @@ export const actions = { dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true }) + if (libraryChanging) { + commit('setCollections', []) + commit('setUserPlaylists', []) + } + commit('addUpdate', library) commit('setLibraryIssues', issues) commit('setLibraryFilterData', filterData) commit('setNumUserPlaylists', numUserPlaylists) commit('setCurrentLibrary', libraryId) - if (libraryChanging) { - commit('setCollections', []) - commit('setUserPlaylists', []) - } return data }) .catch((error) => { From d38058e1d2a17226bac2fb4f7f65f040abadb973 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 31 Dec 2023 15:32:44 -0600 Subject: [PATCH 0277/2145] Fix:Podcast episode time remaining shown on button showing 0 seconds after toggling mark as finished --- client/components/tables/podcast/LazyEpisodeRow.vue | 4 +++- client/pages/library/_library/podcast/latest.vue | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/components/tables/podcast/LazyEpisodeRow.vue b/client/components/tables/podcast/LazyEpisodeRow.vue index d2b106fe..fecf7758 100644 --- a/client/components/tables/podcast/LazyEpisodeRow.vue +++ b/client/components/tables/podcast/LazyEpisodeRow.vue @@ -135,7 +135,9 @@ export default { if (this.streamIsPlaying) return 'Playing' if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0) if (this.userIsFinished) return 'Finished' - const remaining = Math.floor(this.itemProgress.duration - this.itemProgress.currentTime) + + const duration = this.itemProgress.duration || this.episode?.duration || 0 + const remaining = Math.floor(duration - this.itemProgress.currentTime) return `${this.$elapsedPretty(remaining)} left` } }, diff --git a/client/pages/library/_library/podcast/latest.vue b/client/pages/library/_library/podcast/latest.vue index d0565816..e69e055f 100644 --- a/client/pages/library/_library/podcast/latest.vue +++ b/client/pages/library/_library/podcast/latest.vue @@ -155,7 +155,9 @@ export default { if (this.episodeIdStreaming === episode.id) return this.streamIsPlaying ? 'Streaming' : 'Play' if (!episode.progress) return this.$elapsedPretty(episode.duration) if (episode.progress.isFinished) return 'Finished' - var remaining = Math.floor(episode.progress.duration - episode.progress.currentTime) + + const duration = episode.progress.duration || episode.duration + const remaining = Math.floor(duration - episode.progress.currentTime) return `${this.$elapsedPretty(remaining)} left` }, playClick(episodeToPlay) { From 81a76593daaa114eabcc75338d878021d8b97b91 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 31 Dec 2023 15:35:17 -0600 Subject: [PATCH 0278/2145] Fix:Merging chapters from multiple audio files with the same chapter titles #2461 --- server/scanner/AudioFileScanner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/scanner/AudioFileScanner.js b/server/scanner/AudioFileScanner.js index ddd994d0..89951025 100644 --- a/server/scanner/AudioFileScanner.js +++ b/server/scanner/AudioFileScanner.js @@ -468,7 +468,7 @@ class AudioFileScanner { audioFiles.length === 1 || audioFiles.length > 1 && audioFiles[0].chapters.length === audioFiles[1].chapters?.length && - audioFiles[0].chapters.every((c, i) => c.title === audioFiles[1].chapters[i].title) + audioFiles[0].chapters.every((c, i) => c.title === audioFiles[1].chapters[i].title && c.start === audioFiles[1].chapters[i].start) ) { libraryScan.addLog(LogLevel.DEBUG, `setChapters: Using embedded chapters in first audio file ${audioFiles[0].metadata?.path}`) chapters = audioFiles[0].chapters.map((c) => ({ ...c })) From 9a2b93fb377b6c18e2a5fc3a8f6b2ac827907e9a Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 31 Dec 2023 15:37:23 -0600 Subject: [PATCH 0279/2145] Version bump v2.7.1 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index fb8be23f..094eb3f7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.7.0", + "version": "2.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.7.0", + "version": "2.7.1", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index e404e7d4..63d7978f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.7.0", + "version": "2.7.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 8bd0b115..a54cc617 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.7.0", + "version": "2.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.7.0", + "version": "2.7.1", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 33f483b0..dca9710b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.7.0", + "version": "2.7.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From b489bf923622c781e1fe07f20699dc4c89d245e4 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 2 Jan 2024 14:24:59 -0600 Subject: [PATCH 0280/2145] Restrict binary manager to Windows or development --- server/Server.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/Server.js b/server/Server.js index 40b3165a..9d2e3996 100644 --- a/server/Server.js +++ b/server/Server.js @@ -121,7 +121,11 @@ class Server { const libraries = await Database.libraryModel.getAllOldLibraries() await this.cronManager.init(libraries) this.apiCacheManager.init() - await this.binaryManager.init() + + // Download ffmpeg & ffprobe if not found (Currently only in use for Windows installs) + if (global.isWin || Logger.isDev) { + await this.binaryManager.init() + } if (Database.serverSettings.scannerDisableWatcher) { Logger.info(`[Server] Watcher is disabled`) From a1e321b153c9ba8821e35b8975639f4fe998d4e0 Mon Sep 17 00:00:00 2001 From: Machou <Machou@users.noreply.github.com> Date: Wed, 3 Jan 2024 20:16:21 +0100 Subject: [PATCH 0281/2145] Update fr.json --- client/strings/fr.json | 210 ++++++++++++++++++++--------------------- 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index 86a64602..3db4b43a 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -1,10 +1,10 @@ { "ButtonAdd": "Ajouter", "ButtonAddChapters": "Ajouter le chapitre", - "ButtonAddDevice": "Add Device", - "ButtonAddLibrary": "Add Library", + "ButtonAddDevice": "Ajouter un appareil", + "ButtonAddLibrary": "Ajouter une bibliothèque", "ButtonAddPodcasts": "Ajouter des podcasts", - "ButtonAddUser": "Add User", + "ButtonAddUser": "Ajouter un utilisateur", "ButtonAddYourFirstLibrary": "Ajouter votre première bibliothèque", "ButtonApply": "Appliquer", "ButtonApplyChapters": "Appliquer les chapitres", @@ -62,7 +62,7 @@ "ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série", "ButtonReScan": "Nouvelle analyse", "ButtonReset": "Réinitialiser", - "ButtonResetToDefault": "Reset to default", + "ButtonResetToDefault": "Réinitialiser aux valeurs par défaut", "ButtonRestore": "Rétablir", "ButtonSave": "Sauvegarder", "ButtonSaveAndClose": "Sauvegarder et Fermer", @@ -87,9 +87,9 @@ "ButtonUserEdit": "Modifier l’utilisateur {0}", "ButtonViewAll": "Afficher tout", "ButtonYes": "Oui", - "ErrorUploadFetchMetadataAPI": "Error fetching metadata", - "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", - "ErrorUploadLacksTitle": "Must have a title", + "ErrorUploadFetchMetadataAPI": "Erreur lors de la récupération des métadonnées", + "ErrorUploadFetchMetadataNoResults": "Impossible de récupérer les métadonnées - essayez de mettre à jour le titre et/ou l’auteur.", + "ErrorUploadLacksTitle": "Doit avoir un titre", "HeaderAccount": "Compte", "HeaderAdvanced": "Avancé", "HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise", @@ -130,15 +130,15 @@ "HeaderManageTags": "Gérer les étiquettes", "HeaderMapDetails": "Édition en masse", "HeaderMatch": "Chercher", - "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", - "HeaderMetadataToEmbed": "Métadonnée à intégrer", + "HeaderMetadataOrderOfPrecedence": "Ordre de priorité des métadonnées", + "HeaderMetadataToEmbed": "Métadonnées à intégrer", "HeaderNewAccount": "Nouveau compte", "HeaderNewLibrary": "Nouvelle bibliothèque", "HeaderNotifications": "Notifications", - "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", - "HeaderOpenRSSFeed": "Ouvrir Flux RSS", + "HeaderOpenIDConnectAuthentication": "Authentification via OpenID Connect", + "HeaderOpenRSSFeed": "Ouvrir un flux RSS", "HeaderOtherFiles": "Autres fichiers", - "HeaderPasswordAuthentication": "Password Authentication", + "HeaderPasswordAuthentication": "Authentification par mot de passe", "HeaderPermissions": "Permissions", "HeaderPlayerQueue": "Liste d’écoute", "HeaderPlaylist": "Liste de lecture", @@ -187,11 +187,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", + "LabelAdminUsersOnly": "Administrateurs uniquement", "LabelAll": "Tout", "LabelAllUsers": "Tous les utilisateurs", - "LabelAllUsersExcludingGuests": "All users excluding guests", - "LabelAllUsersIncludingGuests": "All users including guests", + "LabelAllUsersExcludingGuests": "Tous les utilisateurs à l’exception des invités", + "LabelAllUsersIncludingGuests": "Tous les utilisateurs, y compris les invités", "LabelAlreadyInYourLibrary": "Déjà dans la bibliothèque", "LabelAppend": "Ajouter", "LabelAuthor": "Auteur", @@ -199,29 +199,29 @@ "LabelAuthorLastFirst": "Auteur (Nom, Prénom)", "LabelAuthors": "Auteurs", "LabelAutoDownloadEpisodes": "Téléchargement automatique d’épisode", - "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", - "LabelAutoRegisterDescription": "Automatically create new users after logging in", - "LabelBackToUser": "Revenir à l’Utilisateur", - "LabelBackupLocation": "Backup Location", + "LabelAutoFetchMetadata": "Recherche automatique de métadonnées", + "LabelAutoFetchMetadataHelp": "Récupère les métadonnées du titre, de l’auteur et de la série pour simplifier le téléchargement. Il se peut que des métadonnées supplémentaires doivent être ajoutées après le téléchargement.", + "LabelAutoLaunch": "Lancement automatique", + "LabelAutoLaunchDescription": "Redirection automatique vers le fournisseur d'authentification lors de la navigation vers la page de connexion (chemin de remplacement manuel <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Enregistrement automatique", + "LabelAutoRegisterDescription": "Créer automatiquement de nouveaux utilisateurs après la connexion", + "LabelBackToUser": "Retour à l’utilisateur", + "LabelBackupLocation": "Emplacement de la sauvegarde", "LabelBackupsEnableAutomaticBackups": "Activer les sauvegardes automatiques", - "LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes Enregistrées dans /metadata/backups", + "LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes enregistrées dans /metadata/backups", "LabelBackupsMaxBackupSize": "Taille maximale de la sauvegarde (en Go)", "LabelBackupsMaxBackupSizeHelp": "Afin de prévenir les mauvaises configuration, la sauvegarde échouera si elle excède la taille limite.", - "LabelBackupsNumberToKeep": "Nombre de sauvegardes à maintenir", - "LabelBackupsNumberToKeepHelp": "Une seule sauvegarde sera effacée à la fois. Si vous avez plus de sauvegardes à effacer, vous devrez le faire manuellement.", + "LabelBackupsNumberToKeep": "Nombre de sauvegardes à conserver", + "LabelBackupsNumberToKeepHelp": "Seule une sauvegarde sera supprimée à la fois. Si vous avez déjà plus de sauvegardes à effacer, vous devez les supprimer manuellement.", "LabelBitrate": "Bitrate", "LabelBooks": "Livres", - "LabelButtonText": "Button Text", + "LabelButtonText": "Texte du bouton", "LabelChangePassword": "Modifier le mot de passe", "LabelChannels": "Canaux", "LabelChapters": "Chapitres", - "LabelChaptersFound": "Chapitres trouvés", - "LabelChapterTitle": "Titres du chapitre", - "LabelClickForMoreInfo": "Click for more info", + "LabelChaptersFound": "chapitres trouvés", + "LabelChapterTitle": "Titre du chapitre", + "LabelClickForMoreInfo": "Cliquez ici pour plus d’informations", "LabelClosePlayer": "Fermer le lecteur", "LabelCodec": "Codec", "LabelCollapseSeries": "Réduire les séries", @@ -235,20 +235,20 @@ "LabelCover": "Couverture", "LabelCoverImageURL": "URL vers l’image de couverture", "LabelCreatedAt": "Créé le", - "LabelCronExpression": "Expression Cron", - "LabelCurrent": "Courrant", - "LabelCurrently": "En ce moment :", - "LabelCustomCronExpression": "Expression cron personnalisée:", - "LabelDatetime": "Datetime", - "LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", + "LabelCronExpression": "Expression cron", + "LabelCurrent": "Actuel", + "LabelCurrently": "Actuellement :", + "LabelCustomCronExpression": "Expression cron personnalisée :", + "LabelDatetime": "Datetime", // need review with context + "LabelDeleteFromFileSystemCheckbox": "Supprimer du système de fichiers (décocher pour ne supprimer que de la base de données)", "LabelDescription": "Description", "LabelDeselectAll": "Tout déselectionner", "LabelDevice": "Appareil", "LabelDeviceInfo": "Détail de l’appareil", - "LabelDeviceIsAvailableTo": "Device is available to...", + "LabelDeviceIsAvailableTo": "L’appareil est disponible pour…", "LabelDirectory": "Répertoire", - "LabelDiscFromFilename": "Disque depuis le fichier", - "LabelDiscFromMetadata": "Disque depuis les métadonnées", + "LabelDiscFromFilename": "Disque à partir du fichier", // need review with context + "LabelDiscFromMetadata": "Disque à partir des métadonnées", // need review with context "LabelDiscover": "Découvrir", "LabelDownload": "Téléchargement", "LabelDownloadNEpisodes": "Télécharger {0} épisode(s)", @@ -271,17 +271,17 @@ "LabelExample": "Exemple", "LabelExplicit": "Restriction", "LabelFeedURL": "URL du flux", - "LabelFetchingMetadata": "Fetching Metadata", + "LabelFetchingMetadata": "Récupération des métadonnées", "LabelFile": "Fichier", "LabelFileBirthtime": "Création du fichier", "LabelFileModified": "Modification du fichier", "LabelFilename": "Nom de fichier", - "LabelFilterByUser": "Filtrer par l’utilisateur", + "LabelFilterByUser": "Filtrer par utilisateur", "LabelFindEpisodes": "Trouver des épisodes", - "LabelFinished": "Fini(e)", + "LabelFinished": "Terminé", // need review with context "LabelFolder": "Dossier", "LabelFolders": "Dossiers", - "LabelFontFamily": "Famille de polices", + "LabelFontFamily": "Polices de caractères", "LabelFontScale": "Taille de la police de caractère", "LabelFormat": "Format", "LabelGenre": "Genre", @@ -289,16 +289,16 @@ "LabelHardDeleteFile": "Suppression du fichier", "LabelHasEbook": "Dispose d’un livre numérique", "LabelHasSupplementaryEbook": "Dispose d’un livre numérique supplémentaire", - "LabelHighestPriority": "Highest priority", + "LabelHighestPriority": "Priorité la plus élevée", "LabelHost": "Hôte", "LabelHour": "Heure", - "LabelIcon": "Icone", - "LabelImageURLFromTheWeb": "Image URL from the web", - "LabelIncludeInTracklist": "Inclure dans la liste des pistes", + "LabelIcon": "Icône", + "LabelImageURLFromTheWeb": "URL de l’image à partir du web", + "LabelIncludeInTracklist": "Inclure dans la liste de lecture", "LabelIncomplete": "Incomplet", "LabelInProgress": "En cours", "LabelInterval": "Intervalle", - "LabelIntervalCustomDailyWeekly": "Journalier / Hebdomadaire personnalisé", + "LabelIntervalCustomDailyWeekly": "Personnaliser quotidiennement / hebdomadairement", "LabelIntervalEvery12Hours": "Toutes les 12 heures", "LabelIntervalEvery15Minutes": "Toutes les 15 minutes", "LabelIntervalEvery2Hours": "Toutes les 2 heures", @@ -331,22 +331,22 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date", - "LabelLowestPriority": "Lowest Priority", - "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", + "LabelLowestPriority": "Priorité la plus basse", + "LabelMatchExistingUsersBy": "Faire correspondre les utilisateurs existants par", + "LabelMatchExistingUsersByDescription": "Utilisé pour connecter les utilisateurs existants. Une fois connectés, les utilisateurs seront associés à un identifiant unique provenant de votre fournisseur SSO.", "LabelMediaPlayer": "Lecteur multimédia", "LabelMediaType": "Type de média", - "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", + "LabelMetadataOrderOfPrecedenceDescription": "Les sources de métadonnées ayant une priorité plus élevée auront la priorité sur celles ayant une priorité moins élevée.", "LabelMetadataProvider": "Fournisseur de métadonnées", - "LabelMetaTag": "Etiquette de métadonnée", - "LabelMetaTags": "Etiquettes de métadonnée", + "LabelMetaTag": "Balise de métadonnée", + "LabelMetaTags": "Balises de métadonnée", "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.", + "LabelMobileRedirectURIs": "URI de redirection mobile autorisés", + "LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche d’URI de redirection valides pour les applications mobiles. Celui par défaut est <code>audiobookshelf://oauth</code>, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l'intégration d'applications tierces. L’utilisation d’un astérisque (<code>*</code>) comme seule entrée autorise n’importe quel URI.", "LabelMore": "Plus", - "LabelMoreInfo": "Plus d’info", + "LabelMoreInfo": "Plus d’informations", "LabelName": "Nom", "LabelNarrator": "Narrateur", "LabelNarrators": "Narrateurs", @@ -358,7 +358,7 @@ "LabelNextScheduledRun": "Prochain lancement prévu", "LabelNoEpisodesSelected": "Aucun épisode sélectionné", "LabelNotes": "Notes", - "LabelNotFinished": "Non terminé(e)", + "LabelNotFinished": "Non terminé", "LabelNotificationAppriseURL": "URL(s) d’Apprise", "LabelNotificationAvailableVariables": "Variables disponibles", "LabelNotificationBodyTemplate": "Modèle de Message", @@ -367,10 +367,10 @@ "LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint", "LabelNotificationsMaxQueueSize": "Nombres de notifications maximum à mettre en attente", "LabelNotificationsMaxQueueSizeHelp": "La limite de notification est de un évènement par seconde. Les notifications seront ignorées si la file d’attente est à son maximum. Cela empêche un flot trop important.", - "LabelNotificationTitleTemplate": "Modèle de Titre", - "LabelNotStarted": "Non Démarré(e)", - "LabelNumberOfBooks": "Nombre de Livres", - "LabelNumberOfEpisodes": "Nombre d’Episodes", + "LabelNotificationTitleTemplate": "Modèle de titre", + "LabelNotStarted": "Pas commencé", + "LabelNumberOfBooks": "Nombre de livres", + "LabelNumberOfEpisodes": "Nombre d’épisodes", "LabelOpenRSSFeed": "Ouvrir le flux RSS", "LabelOverwrite": "Écraser", "LabelPassword": "Mot de passe", @@ -411,7 +411,7 @@ "LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé", "LabelRSSFeedOpen": "Flux RSS ouvert", "LabelRSSFeedPreventIndexing": "Empêcher l’indexation", - "LabelRSSFeedSlug": "Identificateur d’adresse du Flux RSS ", + "LabelRSSFeedSlug": "Balise URL du flux RSS", "LabelRSSFeedURL": "Adresse du flux RSS", "LabelSearchTerm": "Terme de recherche", "LabelSearchTitle": "Titre de recherche", @@ -419,8 +419,8 @@ "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 à...", + "LabelSelectUsers": "Sélectionner les utilisateurs", + "LabelSendEbookToDevice": "Envoyer le livre numérique à…", "LabelSequence": "Séquence", "LabelSeries": "Séries", "LabelSeriesName": "Nom de la série", @@ -447,13 +447,13 @@ "LabelSettingsHomePageBookshelfView": "La page d’accueil utilise la vue étagère", "LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère", "LabelSettingsParseSubtitles": "Analyser les sous-titres", - "LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du Livre Audio.<br>Les sous-titres doivent être séparés par « - »<br>i.e. « Titre du Livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »", + "LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du livre audio.<br>Les sous-titres doivent être séparés par « - »<br>c’est-à-dire : « Titre du livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »", "LabelSettingsPreferMatchedMetadata": "Préférer les métadonnées par correspondance", "LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de l’article lors d’une recherche par correspondance rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.", "LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN", "LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ISBN", "LabelSettingsSortingIgnorePrefixes": "Ignorer les préfixes lors du tri", - "LabelSettingsSortingIgnorePrefixesHelp": "i.e. pour le préfixe « le », le livre avec pour titre « Le Titre du Livre » sera trié en tant que « Titre du Livre, Le »", + "LabelSettingsSortingIgnorePrefixesHelp": "c’est-à-dire : pour le préfixe « le », le livre avec pour titre « Le Titre du Livre » sera trié en tant que « Titre du Livre, Le »", "LabelSettingsSquareBookCovers": "Utiliser des couvertures carrées", "LabelSettingsSquareBookCoversHelp": "Préférer les couvertures carrées par rapport aux couvertures standards de ratio 1.6:1.", "LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les articles", @@ -461,30 +461,30 @@ "LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles", "LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items", "LabelSettingsTimeFormat": "Format d’heure", - "LabelShowAll": "Afficher Tout", + "LabelShowAll": "Tout afficher", "LabelSize": "Taille", "LabelSleepTimer": "Minuterie", - "LabelSlug": "Slug", + "LabelSlug": "Balise", "LabelStart": "Démarrer", "LabelStarted": "Démarré", "LabelStartedAt": "Démarré à", - "LabelStartTime": "Heure de Démarrage", + "LabelStartTime": "Heure de démarrage", "LabelStatsAudioTracks": "Pistes Audios", "LabelStatsAuthors": "Auteurs", - "LabelStatsBestDay": "Meilleur Jour", - "LabelStatsDailyAverage": "Moyenne Journalière", + "LabelStatsBestDay": "Meilleur jour", + "LabelStatsDailyAverage": "Moyenne journalière", "LabelStatsDays": "Jours", "LabelStatsDaysListened": "Jours d’écoute", "LabelStatsHours": "Heures", - "LabelStatsInARow": "d’affilé(s)", + "LabelStatsInARow": "d’affilée(s)", "LabelStatsItemsFinished": "Articles terminés", - "LabelStatsItemsInLibrary": "Articles dans la Bibliothèque", + "LabelStatsItemsInLibrary": "Articles dans la bibliothèque", "LabelStatsMinutes": "minutes", "LabelStatsMinutesListening": "Minutes d’écoute", - "LabelStatsOverallDays": "Jours au total", - "LabelStatsOverallHours": "Heures au total", + "LabelStatsOverallDays": "Nombre total de jours", + "LabelStatsOverallHours": "Nombre total d'heures", "LabelStatsWeekListening": "Écoute de la semaine", - "LabelSubtitle": "Sous-Titre", + "LabelSubtitle": "Sous-titre", "LabelSupportedFileTypes": "Types de fichiers supportés", "LabelTag": "Étiquette", "LabelTags": "Étiquettes", @@ -496,23 +496,23 @@ "LabelThemeLight": "Clair", "LabelTimeBase": "Base de temps", "LabelTimeListened": "Temps d’écoute", - "LabelTimeListenedToday": "Nombres d’écoutes Aujourd’hui", + "LabelTimeListenedToday": "Nombres d’écoutes aujourd’hui", "LabelTimeRemaining": "{0} restantes", "LabelTimeToShift": "Temps de décalage en secondes", "LabelTitle": "Titre", - "LabelToolsEmbedMetadata": "Métadonnées Intégrées", + "LabelToolsEmbedMetadata": "Métadonnées intégrées", "LabelToolsEmbedMetadataDescription": "Intègre les métadonnées au fichier audio avec la couverture et les chapitres.", - "LabelToolsMakeM4b": "Créer un fichier Livre Audio M4B", - "LabelToolsMakeM4bDescription": "Génère un fichier Livre Audio .M4B avec intégration des métadonnées, image de couverture et les chapitres.", + "LabelToolsMakeM4b": "Créer un fichier livre audio M4B", + "LabelToolsMakeM4bDescription": "Génère un fichier livre audio .M4B avec intégration des métadonnées, image de couverture et les chapitres.", "LabelToolsSplitM4b": "Scinde le fichier M4B en fichiers MP3", "LabelToolsSplitM4bDescription": "Créer plusieurs fichier MP3 à partir du découpage par chapitre, en incluant les métadonnées, l’image de couverture et les chapitres.", - "LabelTotalDuration": "Durée Totale", + "LabelTotalDuration": "Durée totale", "LabelTotalTimeListened": "Temps d’écoute total", "LabelTrackFromFilename": "Piste depuis le fichier", "LabelTrackFromMetadata": "Piste depuis les métadonnées", "LabelTracks": "Pistes", "LabelTracksMultiTrack": "Piste multiple", - "LabelTracksNone": "No tracks", + "LabelTracksNone": "Aucune piste", "LabelTracksSingleTrack": "Piste simple", "LabelType": "Type", "LabelUnabridged": "Version intégrale", @@ -524,9 +524,9 @@ "LabelUpdateDetailsHelp": "Autoriser la mise à jour des détails existants lorsqu’une correspondance est trouvée", "LabelUploaderDragAndDrop": "Glisser et déposer des fichiers ou dossiers", "LabelUploaderDropFiles": "Déposer des fichiers", - "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", + "LabelUploaderItemFetchMetadataHelp": "Récupérer automatiquement le titre, l’auteur et la série", "LabelUseChapterTrack": "Utiliser la piste du chapitre", - "LabelUseFullTrack": "Utiliser la piste Complète", + "LabelUseFullTrack": "Utiliser la piste complète", "LabelUser": "Utilisateur", "LabelUsername": "Nom d’utilisateur", "LabelValue": "Valeur", @@ -545,10 +545,10 @@ "MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les sauvegardes n’incluent pas les fichiers de votre bibliothèque.", "MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer l’option suivante pour autoriser la recherche par correspondance à écraser les données existantes.", "MessageBookshelfNoCollections": "Vous n’avez pas encore de collections", - "MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0}: {1} »", + "MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0} : {1} »", "MessageBookshelfNoRSSFeeds": "Aucun flux RSS n’est ouvert", "MessageBookshelfNoSeries": "Vous n’avez aucune série", - "MessageChapterEndIsAfter": "Le Chapitre Fin est situé à la fin de votre Livre Audio", + "MessageChapterEndIsAfter": "La fin du chapitre se situe après la fin de votre livre audio.", "MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0", "MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre", "MessageChapterErrorStartLtPrev": "Horodatage invalide car il doit débuter au moins après le précédent chapitre", @@ -558,15 +558,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?", + "MessageConfirmDeleteLibraryItem": "Cette opération supprimera l’élément de la base de données et de votre système de fichiers. Êtes-vous sûr ?", + "MessageConfirmDeleteLibraryItems": "Cette opération supprimera {0} éléments de la base de données et de votre système de fichiers. Êtes-vous sûr ?", "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?", + "MessageConfirmQuickEmbed": "Attention ! L’intégration rapide ne sauvegardera pas vos fichiers audio. Assurez-vous d’avoir effectuer une sauvegarde de vos fichiers audio.<br><br>Souhaitez-vous continuer ?", "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} » ?", @@ -581,16 +581,16 @@ "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?", + "MessageConfirmReScanLibraryItems": "Êtes-vous sûr de vouloir re-analyser {0} éléments ?", "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", + "MessageDragFilesIntoTrackOrder": "Faites glisser les fichiers dans l’ordre correct des pistes", "MessageEmbedFinished": "Intégration terminée !", "MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement", - "MessageFeedURLWillBe": "l’URL du flux sera {0}", + "MessageFeedURLWillBe": "L’URL du flux sera {0}", "MessageFetching": "Récupération…", - "MessageForceReScanDescription": "Analysera tous les fichiers de nouveau. Les étiquettes ID3 des fichiers audios, fichiers OPF, et les fichiers textes seront analysés comme s’ils étaient nouveaux.", - "MessageImportantNotice": "Information Importante !", + "MessageForceReScanDescription": "analysera de nouveau tous les fichiers. Les étiquettes ID3 des fichiers audio, les fichiers OPF et les fichiers texte seront analysés comme s’ils étaient nouveaux.", + "MessageImportantNotice": "Information importante !", "MessageInsertChapterBelow": "Insérer le chapitre ci-dessous", "MessageItemsSelected": "{0} articles sélectionnés", "MessageItemsUpdated": "{0} articles mis à jour", @@ -646,13 +646,13 @@ "MessageRemoveChapter": "Supprimer le chapitre", "MessageRemoveEpisodes": "Suppression de {0} épisode(s)", "MessageRemoveFromPlayerQueue": "Supprimer de la liste d’écoute", - "MessageRemoveUserWarning": "Êtes-vous certain de vouloir supprimer définitivement l’utilisateur « {0} » ?", + "MessageRemoveUserWarning": "Êtes-vous sûr de vouloir supprimer définitivement l’utilisateur « {0} » ?", "MessageReportBugsAndContribute": "Remonter des anomalies, demander des fonctionnalités et contribuer sur", - "MessageResetChaptersConfirm": "Êtes-vous certain de vouloir réinitialiser les chapitres et annuler les changements effectués ?", - "MessageRestoreBackupConfirm": "Êtes-vous certain de vouloir restaurer la sauvegarde créée le", + "MessageResetChaptersConfirm": "Êtes-vous sûr de vouloir réinitialiser les chapitres et annuler les changements effectués ?", + "MessageRestoreBackupConfirm": "Êtes-vous sûr de vouloir restaurer la sauvegarde créée le", "MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.<br /><br />Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br /><br />Tous les clients utilisant votre serveur seront automatiquement mis à jour.", "MessageSearchResultsFor": "Résultats de recherche pour", - "MessageSelected": "{0} selected", + "MessageSelected": "{0} sélectionnés", "MessageServerCouldNotBeReached": "Serveur inaccessible", "MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre", "MessageStartPlaybackAtTime": "Démarrer la lecture pour « {0} » à {1} ?", @@ -663,10 +663,10 @@ "MessageValidCronExpression": "Expression cron valide", "MessageWatcherIsDisabledGlobally": "La surveillance est désactivée par un paramètre global du serveur", "MessageXLibraryIsEmpty": "La bibliothèque {0} est vide !", - "MessageYourAudiobookDurationIsLonger": "La durée de votre Livre Audio est plus longue que la durée trouvée", - "MessageYourAudiobookDurationIsShorter": "La durée de votre Livre Audio est plus courte que la durée trouvée", + "MessageYourAudiobookDurationIsLonger": "La durée de votre livre audio est plus longue que la durée trouvée", + "MessageYourAudiobookDurationIsShorter": "La durée de votre livre audio est plus courte que la durée trouvée", "NoteChangeRootPassword": "seul l’utilisateur « root » peut utiliser un mot de passe vide", - "NoteChapterEditorTimes": "Information : l’horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du Livre Audio.", + "NoteChapterEditorTimes": "Information : l’horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du livre audio.", "NoteFolderPicker": "Information : Les dossiers déjà surveillés ne sont pas affichés", "NoteFolderPickerDebian": "Information : La sélection de dossier sur une installation debian n’est pas finalisée. Merci de renseigner le chemin complet vers votre bibliothèque manuellement.", "NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.", @@ -677,8 +677,8 @@ "PlaceholderNewCollection": "Nom de la nouvelle collection", "PlaceholderNewFolderPath": "Nouveau chemin de dossier", "PlaceholderNewPlaylist": "Nouveau nom de liste de lecture", - "PlaceholderSearch": "Recherche...", - "PlaceholderSearchEpisode": "Recherche d’épisode...", + "PlaceholderSearch": "Recherche…", + "PlaceholderSearchEpisode": "Recherche d’épisode…", "ToastAccountUpdateFailed": "Échec de la mise à jour du compte", "ToastAccountUpdateSuccess": "Compte mis à jour", "ToastAuthorImageRemoveFailed": "Échec de la suppression de l’image", @@ -750,4 +750,4 @@ "ToastSocketFailedToConnect": "Échec de la connexion WebSocket", "ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur", "ToastUserDeleteSuccess": "Utilisateur supprimé" -} \ No newline at end of file +} From 8027c4a06f01f3a13d43cf041af012a4f64d62f5 Mon Sep 17 00:00:00 2001 From: Barnabas Ratki <dewyerer@gmail.com> Date: Wed, 3 Jan 2024 01:36:56 +0100 Subject: [PATCH 0282/2145] Added support for custom metadata providers WiP but already open to feedback --- client/components/app/ConfigSideNav.vue | 5 + .../modals/AddCustomMetadataProviderModal.vue | 104 ++++++++++++ .../tables/CustomMetadataProviderTable.vue | 150 ++++++++++++++++++ client/layouts/default.vue | 8 + .../config/custom-metadata-providers.vue | 45 ++++++ client/store/scanners.js | 31 +++- client/strings/en-us.json | 3 + server/Database.js | 45 ++++++ server/controllers/LibraryController.js | 10 ++ server/controllers/MiscController.js | 90 +++++++++++ server/finders/BookFinder.js | 17 +- server/models/CustomMetadataProvider.js | 58 +++++++ server/providers/CustomProviderAdapter.js | 76 +++++++++ server/routers/ApiRouter.js | 4 + 14 files changed, 642 insertions(+), 4 deletions(-) create mode 100644 client/components/modals/AddCustomMetadataProviderModal.vue create mode 100644 client/components/tables/CustomMetadataProviderTable.vue create mode 100644 client/pages/config/custom-metadata-providers.vue create mode 100644 server/models/CustomMetadataProvider.js create mode 100644 server/providers/CustomProviderAdapter.js diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index c2db0725..e253d1ae 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -109,6 +109,11 @@ export default { id: 'config-authentication', title: this.$strings.HeaderAuthentication, path: '/config/authentication' + }, + { + id: 'config-custom-metadata-providers', + title: this.$strings.HeaderCustomMetadataProviders, + path: '/config/custom-metadata-providers' } ] diff --git a/client/components/modals/AddCustomMetadataProviderModal.vue b/client/components/modals/AddCustomMetadataProviderModal.vue new file mode 100644 index 00000000..1b9f930c --- /dev/null +++ b/client/components/modals/AddCustomMetadataProviderModal.vue @@ -0,0 +1,104 @@ +<template> + <modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing"> + <template #outer> + <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden"> + <p class="text-3xl text-white truncate">Add custom metadata provider</p> + </div> + </template> + <form @submit.prevent="submitForm"> + <div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh"> + <div class="w-full p-8"> + <div class="w-full mb-4"> + <ui-text-input-with-label v-model="newName" :label="$strings.LabelName" /> + </div> + <div class="w-full mb-4"> + <ui-text-input-with-label v-model="newUrl" :label="$strings.LabelUrl" /> + </div> + <div class="w-full mb-4"> + <ui-text-input-with-label v-model="newApiKey" :label="$strings.LabelApiKey" type="password" /> + </div> + <div class="flex pt-4 px-2"> + <div class="flex-grow" /> + <ui-btn color="success" type="submit">{{ $strings.ButtonAdd }}</ui-btn> + </div> + </div> + </div> + </form> + </modals-modal> +</template> + +<script> +export default { + props: { + value: Boolean, + }, + data() { + return { + processing: false, + newName: "", + newUrl: "", + newApiKey: "", + } + }, + watch: { + show: { + handler(newVal) { + if (newVal) { + this.init() + } + } + } + }, + computed: { + show: { + get() { + return this.value + }, + set(val) { + this.$emit('input', val) + } + }, + }, + methods: { + close() { + // Force close when navigating - used in the table + if (this.$refs.modal) this.$refs.modal.setHide() + }, + submitForm() { + if (!this.newName || !this.newUrl || !this.newApiKey) { + this.$toast.error('Must add name, url and API key') + return + } + + this.processing = true + this.$axios + .$patch('/api/custom-metadata-providers/admin', { + name: this.newName, + url: this.newUrl, + apiKey: this.newApiKey, + }) + .then((data) => { + this.processing = false + if (data.error) { + this.$toast.error(`Failed to add provider: ${data.error}`) + } else { + this.$toast.success('New provider added') + this.show = false + } + }) + .catch((error) => { + this.processing = false + console.error('Failed to add provider', error) + this.$toast.error('Failed to add provider') + }) + }, + init() { + this.processing = false + this.newName = "" + this.newUrl = "" + this.newApiKey = "" + } + }, + mounted() {} +} +</script> diff --git a/client/components/tables/CustomMetadataProviderTable.vue b/client/components/tables/CustomMetadataProviderTable.vue new file mode 100644 index 00000000..8104cede --- /dev/null +++ b/client/components/tables/CustomMetadataProviderTable.vue @@ -0,0 +1,150 @@ +<template> + <div> + <div class="text-center"> + <table id="providers"> + <tr> + <th>{{ $strings.LabelName }}</th> + <th>{{ $strings.LabelUrl }}</th> + <th>{{ $strings.LabelApiKey }}</th> + <th class="w-12"></th> + </tr> + <tr v-for="provider in providers" :key="provider.id"> + <td class="text-sm">{{ provider.name }}</td> + <td class="text-sm">{{ provider.url }}</td> + <td class="text-sm"> + <span class="custom-provider-api-key">{{ provider.apiKey }}</span> + </td> + <td class="py-0"> + <div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="removeProvider(provider)"> + <button type="button" :aria-label="$strings.ButtonDelete" class="material-icons text-base">delete</button> + </div> + </td> + </tr> + </table> + </div> + </div> +</template> + +<script> +export default { + data() { + return { + providers: [], + } + }, + methods: { + addedProvider(provider) { + if (!Array.isArray(this.providers)) return + + this.providers.push(provider) + }, + removedProvider(provider) { + this.providers = this.providers.filter((p) => p.id !== provider.id) + }, + removeProvider(provider) { + this.$axios + .$delete(`/api/custom-metadata-providers/admin/${provider.id}`) + .then((data) => { + if (data.error) { + this.$toast.error(`Failed to remove provider: ${data.error}`) + } else { + this.$toast.success('Provider removed') + } + }) + .catch((error) => { + console.error('Failed to remove provider', error) + this.$toast.error('Failed to remove provider') + }) + }, + loadProviders() { + this.$axios.$get('/api/custom-metadata-providers/admin') + .then((res) => { + this.providers = res.providers + }) + .catch((error) => { + console.error('Failed', error) + }) + }, + init(attempts = 0) { + if (!this.$root.socket) { + if (attempts > 10) { + return console.error('Failed to setup socket listeners') + } + setTimeout(() => { + this.init(++attempts) + }, 250) + return + } + this.$root.socket.on('custom_metadata_provider_added', this.addedProvider) + this.$root.socket.on('custom_metadata_provider_removed', this.removedProvider) + } + }, + mounted() { + this.loadProviders() + this.init() + }, + beforeDestroy() { + if (this.$refs.addModal) { + this.$refs.addModal.close() + } + + if (this.$root.socket) { + this.$root.socket.off('custom_metadata_provider_added', this.addedProvider) + this.$root.socket.off('custom_metadata_provider_removed', this.removedProvider) + } + } +} +</script> + +<style> +#providers { + table-layout: fixed; + border-collapse: collapse; + border: 1px solid #474747; + width: 100%; +} + +#providers td, +#providers th { + /* border: 1px solid #2e2e2e; */ + padding: 8px 8px; + text-align: left; +} + +#providers td.py-0 { + padding: 0px 8px; +} + +#providers tr:nth-child(even) { + background-color: #373838; +} + +#providers tr:nth-child(odd) { + background-color: #2f2f2f; +} + +#providers tr:hover { + background-color: #444; +} + +#providers th { + font-size: 0.8rem; + font-weight: 600; + padding-top: 5px; + padding-bottom: 5px; + background-color: #272727; +} + +.custom-provider-api-key { + padding: 1px; + background-color: #272727; + border-radius: 4px; + color: transparent; + transition: color, background-color 0.5s ease; +} + +.custom-provider-api-key:hover { + background-color: transparent; + color: white; +} +</style> diff --git a/client/layouts/default.vue b/client/layouts/default.vue index c3cc3484..1d33c44c 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -328,6 +328,9 @@ export default { this.$store.commit('libraries/setEReaderDevices', data.ereaderDevices) }, + customMetadataProvidersChanged() { + this.$store.dispatch('scanners/reFetchCustom') + }, initializeSocket() { this.socket = this.$nuxtSocket({ name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod', @@ -406,6 +409,10 @@ export default { this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete) this.socket.on('admin_message', this.adminMessageEvt) + + // Custom metadata provider Listeners + this.socket.on('custom_metadata_provider_added', this.customMetadataProvidersChanged) + this.socket.on('custom_metadata_provider_removed', this.customMetadataProvidersChanged) }, showUpdateToast(versionData) { var ignoreVersion = localStorage.getItem('ignoreVersion') @@ -541,6 +548,7 @@ export default { window.addEventListener('keydown', this.keyDown) this.$store.dispatch('libraries/load') + this.$store.dispatch('scanners/reFetchCustom') this.initLocalStorage() diff --git a/client/pages/config/custom-metadata-providers.vue b/client/pages/config/custom-metadata-providers.vue new file mode 100644 index 00000000..10fdb21b --- /dev/null +++ b/client/pages/config/custom-metadata-providers.vue @@ -0,0 +1,45 @@ +<template> + <div id="authentication-settings"> + <app-settings-content :header-text="$strings.HeaderCustomMetadataProviders"> + <template #header-items> + <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2"> + <a href="https://www.audiobookshelf.org/guides/#" 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="setShowAddModal()">{{ $strings.ButtonAdd }}</ui-btn> + </template> + + <tables-custom-metadata-provider-table class="pt-2" /> + <modals-add-custom-metadata-provider-modal ref="addModal" v-model="showAddModal" /> + </app-settings-content> + </div> +</template> + +<script> +export default { + async asyncData({ store, redirect }) { + if (!store.getters['user/getIsAdminOrUp']) { + redirect('/') + return + } + return {} + }, + data() { + return { + showAddModal: false, + } + }, + methods: { + setShowAddModal() { + this.showAddModal = true + } + } +} +</script> + +<style> + +</style> diff --git a/client/store/scanners.js b/client/store/scanners.js index ccdc1791..32878a6a 100644 --- a/client/store/scanners.js +++ b/client/store/scanners.js @@ -73,6 +73,33 @@ export const state = () => ({ export const getters = {} -export const actions = {} +export const actions = { + reFetchCustom({ dispatch, commit }) { + return this.$axios + .$get(`/api/custom-metadata-providers`) + .then((data) => { + const providers = data.providers -export const mutations = {} \ No newline at end of file + commit('setCustomProviders', providers) + return data + }) + .catch((error) => { + console.error('Failed', error) + return false + }) + }, +} + +export const mutations = { + setCustomProviders(state, providers) { + // clear previous values, and add new values to the end + state.providers = state.providers.filter((p) => !p.value.startsWith("custom-")); + state.providers = [ + ...state.providers, + ...providers.map((p) => {return { + text: p.name, + value: p.slug, + }}) + ] + }, +} \ No newline at end of file diff --git a/client/strings/en-us.json b/client/strings/en-us.json index f69175fd..9dfde095 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -96,6 +96,7 @@ "HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudioTracks": "Audio Tracks", "HeaderAuthentication": "Authentication", + "HeaderCustomMetadataProviders": "Custom metadata providers", "HeaderBackups": "Backups", "HeaderChangePassword": "Change Password", "HeaderChapters": "Chapters", @@ -540,6 +541,8 @@ "LabelYourBookmarks": "Your Bookmarks", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Your Progress", + "LabelUrl": "URL", + "LabelApiKey": "API Key", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.", diff --git a/server/Database.js b/server/Database.js index fd606bac..bbea7352 100644 --- a/server/Database.js +++ b/server/Database.js @@ -132,6 +132,11 @@ class Database { return this.models.playbackSession } + /** @type {typeof import('./models/CustomMetadataProvider')} */ + get customMetadataProviderModel() { + return this.models.customMetadataProvider + } + /** * Check if db file exists * @returns {boolean} @@ -245,6 +250,7 @@ class Database { require('./models/Feed').init(this.sequelize) require('./models/FeedEpisode').init(this.sequelize) require('./models/Setting').init(this.sequelize) + require('./models/CustomMetadataProvider').init(this.sequelize) return this.sequelize.sync({ force, alter: false }) } @@ -694,6 +700,45 @@ class Database { }) } + /** + * Returns true if a custom provider with the given slug exists + * @param {string} providerSlug + * @return {boolean} + */ + async doesCustomProviderExistBySlug(providerSlug) { + const id = providerSlug.split("custom-")[1] + + if (!id) { + return false + } + + return !!await this.customMetadataProviderModel.findByPk(id) + } + + /** + * Removes a custom metadata provider + * @param {string} id + */ + async removeCustomMetadataProviderById(id) { + // destroy metadta provider + await this.customMetadataProviderModel.destroy({ + where: { + id, + } + }) + + const slug = `custom-${id}`; + + // fallback libraries using it to google + await this.libraryModel.update({ + provider: "google", + }, { + where: { + provider: slug, + } + }); + } + /** * Clean invalid records in database * Series should have atleast one Book diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 70baff85..304ca4f0 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -51,6 +51,11 @@ class LibraryController { } } + // Validate that the custom provider exists if given any + if (newLibraryPayload.provider && newLibraryPayload.provider.startsWith("custom-")) { + await Database.doesCustomProviderExistBySlug(newLibraryPayload.provider) + } + const library = new Library() let currentLargestDisplayOrder = await Database.libraryModel.getMaxDisplayOrder() @@ -175,6 +180,11 @@ class LibraryController { } } + // Validate that the custom provider exists if given any + if (req.body.provider && req.body.provider.startsWith("custom-")) { + await Database.doesCustomProviderExistBySlug(req.body.provider) + } + const hasUpdates = library.update(req.body) // TODO: Should check if this is an update to folder paths or name only if (hasUpdates) { diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index c2272ee6..31f4587b 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -717,5 +717,95 @@ class MiscController { const stats = await adminStats.getStatsForYear(year) res.json(stats) } + + /** + * GET: /api/custom-metadata-providers + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async getCustomMetadataProviders(req, res) { + const providers = await Database.customMetadataProviderModel.findAll() + + res.json({ + providers: providers.map((p) => p.toUserJson()), + }) + } + + /** + * GET: /api/custom-metadata-providers/admin + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async getAdminCustomMetadataProviders(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get admin custom metadata providers`) + return res.sendStatus(403) + } + + const providers = await Database.customMetadataProviderModel.findAll() + + res.json({ + providers, + }) + } + + /** + * PATCH: /api/custom-metadata-providers/admin + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async addCustomMetadataProviders(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get admin custom metadata providers`) + return res.sendStatus(403) + } + + const { name, url, apiKey } = req.body; + + if (!name || !url || !apiKey) { + return res.status(500).send(`Invalid patch data`) + } + + const provider = await Database.customMetadataProviderModel.create({ + name, + url, + apiKey, + }) + + SocketAuthority.adminEmitter('custom_metadata_provider_added', provider) + + res.json({ + provider, + }) + } + + /** + * DELETE: /api/custom-metadata-providers/admin/:id + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async deleteCustomMetadataProviders(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get admin custom metadata providers`) + return res.sendStatus(403) + } + + const { id } = req.params; + + if (!id) { + return res.status(500).send(`Invalid delete data`) + } + + const provider = await Database.customMetadataProviderModel.findByPk(id); + await Database.removeCustomMetadataProviderById(id); + + SocketAuthority.adminEmitter('custom_metadata_provider_removed', provider) + + res.json({}) + } } module.exports = new MiscController() diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 466c8701..6c35a5fb 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -5,6 +5,7 @@ const iTunes = require('../providers/iTunes') const Audnexus = require('../providers/Audnexus') const FantLab = require('../providers/FantLab') const AudiobookCovers = require('../providers/AudiobookCovers') +const CustomProviderAdapter = require('../providers/CustomProviderAdapter') const Logger = require('../Logger') const { levenshteinDistance, escapeRegExp } = require('../utils/index') @@ -17,6 +18,7 @@ class BookFinder { this.audnexus = new Audnexus() this.fantLab = new FantLab() this.audiobookCovers = new AudiobookCovers() + this.customProviderAdapter = new CustomProviderAdapter() this.providers = ['google', 'itunes', 'openlibrary', 'fantlab', 'audiobookcovers', 'audible', 'audible.ca', 'audible.uk', 'audible.au', 'audible.fr', 'audible.de', 'audible.jp', 'audible.it', 'audible.in', 'audible.es'] @@ -147,6 +149,13 @@ class BookFinder { return books } + async getCustomProviderResults(title, author, providerSlug) { + const books = await this.customProviderAdapter.search(title, author, providerSlug) + if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`) + + return books + } + static TitleCandidates = class { constructor(cleanAuthor) { @@ -315,6 +324,11 @@ class BookFinder { const maxFuzzySearches = !isNaN(options.maxFuzzySearches) ? Number(options.maxFuzzySearches) : 5 let numFuzzySearches = 0 + // Custom providers are assumed to be correct + if (provider.startsWith("custom-")) { + return await this.getCustomProviderResults(title, author, provider) + } + if (!title) return books @@ -397,8 +411,7 @@ class BookFinder { books = await this.getFantLabResults(title, author) } else if (provider === 'audiobookcovers') { books = await this.getAudiobookCoversResults(title) - } - else { + } else { books = await this.getGoogleBooksResults(title, author) } return books diff --git a/server/models/CustomMetadataProvider.js b/server/models/CustomMetadataProvider.js new file mode 100644 index 00000000..4f2488d2 --- /dev/null +++ b/server/models/CustomMetadataProvider.js @@ -0,0 +1,58 @@ +const { DataTypes, Model, Sequelize } = require('sequelize') + +class CustomMetadataProvider extends Model { + constructor(values, options) { + super(values, options) + + /** @type {UUIDV4} */ + this.id + /** @type {string} */ + this.name + /** @type {string} */ + this.url + /** @type {string} */ + this.apiKey + } + + getSlug() { + return `custom-${this.id}` + } + + toUserJson() { + return { + name: this.name, + id: this.id, + slug: this.getSlug() + } + } + + static findByPk(id) { + this.findOne({ + where: { + id, + } + }) + } + + /** + * Initialize model + * @param {import('../Database').sequelize} sequelize + */ + static init(sequelize) { + super.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + name: DataTypes.STRING, + url: DataTypes.STRING, + apiKey: DataTypes.STRING + }, { + sequelize, + modelName: 'customMetadataProvider' + }) + } +} + +module.exports = CustomMetadataProvider \ No newline at end of file diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js new file mode 100644 index 00000000..1bf5a5ee --- /dev/null +++ b/server/providers/CustomProviderAdapter.js @@ -0,0 +1,76 @@ +const Database = require('../Database') +const axios = require("axios"); +const Logger = require("../Logger"); + +class CustomProviderAdapter { + constructor() { + } + + async search(title, author, providerSlug) { + const providerId = providerSlug.split("custom-")[1] + + console.log(providerId) + const provider = await Database.customMetadataProviderModel.findOne({ + where: { + id: providerId, + } + }); + + if (!provider) { + throw new Error("Custom provider not found for the given id"); + } + + const matches = await axios.get(`${provider.url}/search?query=${encodeURIComponent(title)}${!!author ? `&author=${encodeURIComponent(author)}` : ""}`, { + headers: { + "Authorization": provider.apiKey, + }, + }).then((res) => { + if (!res || !res.data || !Array.isArray(res.data.matches)) return null + return res.data.matches + }).catch(error => { + Logger.error('[CustomMetadataProvider] Search error', error) + return [] + }) + + if (matches === null) { + throw new Error("Custom provider returned malformed response"); + } + + // re-map keys to throw out + return matches.map(({ + title, + subtitle, + author, + narrator, + publisher, + published_year, + description, + cover, + isbn, + asin, + genres, + tags, + language, + duration, + }) => { + return { + title, + subtitle, + author, + narrator, + publisher, + publishedYear: published_year, + description, + cover, + isbn, + asin, + genres, + tags: tags.join(","), + language, + duration, + } + }) + } +} + +module.exports = CustomProviderAdapter \ No newline at end of file diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 3edce256..f78d4539 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -318,6 +318,10 @@ class ApiRouter { this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this)) this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this)) this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this)) + this.router.get('/custom-metadata-providers', MiscController.getCustomMetadataProviders.bind(this)) + this.router.get('/custom-metadata-providers/admin', MiscController.getAdminCustomMetadataProviders.bind(this)) + this.router.patch('/custom-metadata-providers/admin', MiscController.addCustomMetadataProviders.bind(this)) + this.router.delete('/custom-metadata-providers/admin/:id', MiscController.deleteCustomMetadataProviders.bind(this)) } async getDirectories(dir, relpath, excludedDirs, level = 0) { From 08a41e37b4d412ef04cb45e839acda07b1e77cd9 Mon Sep 17 00:00:00 2001 From: Barnabas Ratki <dewyerer@gmail.com> Date: Wed, 3 Jan 2024 20:27:42 +0100 Subject: [PATCH 0283/2145] Add specification --- custom-metadata-provider-specification.yaml | 124 ++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 custom-metadata-provider-specification.yaml diff --git a/custom-metadata-provider-specification.yaml b/custom-metadata-provider-specification.yaml new file mode 100644 index 00000000..3201fbb8 --- /dev/null +++ b/custom-metadata-provider-specification.yaml @@ -0,0 +1,124 @@ +openapi: 3.0.0 +servers: + - url: https://example.com + description: Local server +info: + license: + name: MIT + url: https://opensource.org/licenses/MIT + + + title: Custom Metadata Provider + version: 0.1.0 +security: + - api_key: [] + +paths: + /search: + get: + description: Search for books + operationId: search + summary: Search for books + security: + - api_key: [] + parameters: + - name: query + in: query + required: true + schema: + type: string + - name: author + in: query + required: false + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + matches: + type: array + items: + $ref: "#/components/schemas/BookMetadata" + "400": + description: Bad Request + content: + application/json: + schema: + type: object + properties: + error: + type: string + "401": + description: Unauthorized + content: + application/json: + schema: + type: object + properties: + error: + type: string + "500": + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string +components: + schemas: + BookMetadata: + type: object + properties: + title: + type: string + subtitle: + type: string + author: + type: string + narrator: + type: string + publisher: + type: string + published_year: + type: string + description: + type: string + cover: + type: string + description: URL to the cover image + isbn: + type: string + format: isbn + asin: + type: string + format: asin + genres: + type: array + items: + type: string + tags: + type: array + items: + type: string + language: + type: string + duration: + type: number + format: int64 + description: Duration in seconds + required: + - title + securitySchemes: + api_key: + type: apiKey + name: AUTHORIZATION + in: header + + From 5ea423072be02beb9489e62c51d8aeb023acbeb1 Mon Sep 17 00:00:00 2001 From: Barnabas Ratki <dewyerer@gmail.com> Date: Wed, 3 Jan 2024 20:40:36 +0100 Subject: [PATCH 0284/2145] Small fixes --- server/Database.js | 2 +- server/controllers/LibraryController.js | 4 ++-- server/models/CustomMetadataProvider.js | 2 +- server/providers/CustomProviderAdapter.js | 8 +------- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/server/Database.js b/server/Database.js index bbea7352..302170ac 100644 --- a/server/Database.js +++ b/server/Database.js @@ -705,7 +705,7 @@ class Database { * @param {string} providerSlug * @return {boolean} */ - async doesCustomProviderExistBySlug(providerSlug) { + async doesCustomProviderExistWithSlug(providerSlug) { const id = providerSlug.split("custom-")[1] if (!id) { diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 304ca4f0..b1ab572f 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -53,7 +53,7 @@ class LibraryController { // Validate that the custom provider exists if given any if (newLibraryPayload.provider && newLibraryPayload.provider.startsWith("custom-")) { - await Database.doesCustomProviderExistBySlug(newLibraryPayload.provider) + await Database.doesCustomProviderExistWithSlug(newLibraryPayload.provider) } const library = new Library() @@ -182,7 +182,7 @@ class LibraryController { // Validate that the custom provider exists if given any if (req.body.provider && req.body.provider.startsWith("custom-")) { - await Database.doesCustomProviderExistBySlug(req.body.provider) + await Database.doesCustomProviderExistWithSlug(req.body.provider) } const hasUpdates = library.update(req.body) diff --git a/server/models/CustomMetadataProvider.js b/server/models/CustomMetadataProvider.js index 4f2488d2..9bc175c4 100644 --- a/server/models/CustomMetadataProvider.js +++ b/server/models/CustomMetadataProvider.js @@ -27,7 +27,7 @@ class CustomMetadataProvider extends Model { } static findByPk(id) { - this.findOne({ + return this.findOne({ where: { id, } diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js index 1bf5a5ee..d5f64291 100644 --- a/server/providers/CustomProviderAdapter.js +++ b/server/providers/CustomProviderAdapter.js @@ -8,13 +8,7 @@ class CustomProviderAdapter { async search(title, author, providerSlug) { const providerId = providerSlug.split("custom-")[1] - - console.log(providerId) - const provider = await Database.customMetadataProviderModel.findOne({ - where: { - id: providerId, - } - }); + const provider = await Database.customMetadataProviderModel.findByPk(providerId); if (!provider) { throw new Error("Custom provider not found for the given id"); From 12c6a1baa02b2514b48565a4e030281856b2906d Mon Sep 17 00:00:00 2001 From: Barnabas Ratki <dewyerer@gmail.com> Date: Wed, 3 Jan 2024 20:42:35 +0100 Subject: [PATCH 0285/2145] Fix log messages --- server/controllers/MiscController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 31f4587b..76140dcc 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -759,7 +759,7 @@ class MiscController { */ async addCustomMetadataProviders(req, res) { if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get admin custom metadata providers`) + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to add admin custom metadata providers`) return res.sendStatus(403) } @@ -790,7 +790,7 @@ class MiscController { */ async deleteCustomMetadataProviders(req, res) { if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get admin custom metadata providers`) + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to delete admin custom metadata providers`) return res.sendStatus(403) } From baa65b8155aa21468650f5859c4dc42ece478a6b Mon Sep 17 00:00:00 2001 From: Benjamin Porter <FreedomBen@users.noreply.github.com> Date: Wed, 3 Jan 2024 13:55:43 -0700 Subject: [PATCH 0286/2145] Add tini as PID 1 handler in container image This PR adds `tini` to the container image and uses it as PID 1 when starting the container. This ensures that proper PID 1 signal-handling is implemented and passed to the underlying node.js process, thereby ensuring that the ABS process has a chance to receive and handle signals other than `SIGKILL`, such as the important `SIGINT`. This is somewhat related to #2445 . Without this, the signal handled by 2445 won't be received when running in a container. Some background: In linux, PID 1 has special duties involving signal handling that are different than other processes. Node doesn't properly handle these signals, which can lead to a number of problems ranging from annoying to disruptive. PID 1 also has reaping duties that can lead to resource exhaustion if not properly handled. For example, the container ignores `SIGINT` (Ctrl+C) as well as `docker stop`, which can be annoying in development as you have to kill or wait for the timeout to be reached. In a production environment (such as Kubernetes) this can lead to signal escalation and unnecessarily adds delays to deployments and restarts as K8s has to wait for the timeout to be reached before sending `SIGKILL`. At best this is annoying and unnecessarily adds delays. At worst this can lead to file/data corruption as the process doesn't get a chance to clean anything up when it is sent `SIGKILL`. Without a proper PID 1 to forward signals, only SIGKILL can be used to terminate the running process. --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 25472000..943fc567 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,8 @@ RUN apk update && \ ffmpeg \ make \ python3 \ - g++ + g++ \ + tini COPY --from=tone /usr/local/bin/tone /usr/local/bin/ COPY --from=build /client/dist /client/dist @@ -31,4 +32,5 @@ RUN apk del make python3 g++ EXPOSE 80 +ENTRYPOINT ["tini", "--"] CMD ["node", "index.js"] From 9f909b0d85c6c6961bb7da80a7f2bdce265f07d6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 3 Jan 2024 16:23:17 -0600 Subject: [PATCH 0287/2145] Update:Library folder browser to also work for debian and windows --- .../modals/libraries/EditLibrary.vue | 2 +- ...olderChooser.vue => LazyFolderChooser.vue} | 109 ++++++++++++------ server/controllers/FileSystemController.js | 60 ++++++++-- server/routers/ApiRouter.js | 29 ----- server/utils/fileUtils.js | 63 ++++++++++ 5 files changed, 184 insertions(+), 79 deletions(-) rename client/components/modals/libraries/{FolderChooser.vue => LazyFolderChooser.vue} (58%) diff --git a/client/components/modals/libraries/EditLibrary.vue b/client/components/modals/libraries/EditLibrary.vue index 598f3bcd..d29d7929 100644 --- a/client/components/modals/libraries/EditLibrary.vue +++ b/client/components/modals/libraries/EditLibrary.vue @@ -31,7 +31,7 @@ <ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn> </div> </div> - <modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" /> + <modals-libraries-lazy-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" /> </div> </template> diff --git a/client/components/modals/libraries/FolderChooser.vue b/client/components/modals/libraries/LazyFolderChooser.vue similarity index 58% rename from client/components/modals/libraries/FolderChooser.vue rename to client/components/modals/libraries/LazyFolderChooser.vue index 6383d102..0254f760 100644 --- a/client/components/modals/libraries/FolderChooser.vue +++ b/client/components/modals/libraries/LazyFolderChooser.vue @@ -4,29 +4,32 @@ <span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span> <p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p> </div> - <div v-if="allFolders.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2"> - <p class="font-mono truncate">{{ selectedPath || '\\' }}</p> + <div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2"> + <p class="font-mono truncate">{{ selectedPath || '/' }}</p> </div> - <div v-if="allFolders.length" class="flex bg-primary bg-opacity-50 p-4 folder-container"> + <div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container"> <div class="w-1/2 border-r border-bg h-full overflow-y-auto"> - <div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center" @click="goBack"> + <div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack"> <span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <p class="text-base font-mono px-2">..</p> </div> - <div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200" :class="dir.className" @click="selectDir(dir)"> + <div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)"> <span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p> - <span v-if="dir.dirs && dir.dirs.length && dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span> + <span v-if="dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span> </div> </div> <div class="w-1/2 h-full overflow-y-auto"> - <div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200" @click="selectSubDir(dir)"> + <div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)"> <span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p> </div> </div> + <div v-if="loadingDirs" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/10"> + <ui-loading-indicator /> + </div> </div> - <div v-else-if="loadingFolders" class="py-12 text-center"> + <div v-else-if="initialLoad" class="py-12 text-center"> <p>{{ $strings.MessageLoadingFolders }}</p> </div> <div v-else class="py-12 text-center max-w-sm mx-auto"> @@ -51,11 +54,12 @@ export default { }, data() { return { - loadingFolders: false, - allFolders: [], + initialLoad: false, + loadingDirs: false, + isPosix: true, + rootDirs: [], directories: [], selectedPath: '', - selectedFullPath: '', subdirs: [], level: 0, currentDir: null, @@ -98,59 +102,88 @@ export default { } }, methods: { - goBack() { - var splitPaths = this.selectedPath.split('\\').slice(1) - var prev = splitPaths.slice(0, -1).join('\\') + async goBack() { + let selPath = this.selectedPath.replace(/^\//, '') + var splitPaths = selPath.split('/') - var currDirs = this.allFolders - for (let i = 0; i < splitPaths.length; i++) { - var _dir = currDirs.find((dir) => dir.dirname === splitPaths[i]) - if (_dir && _dir.path.slice(1) === prev) { - this.directories = currDirs - this.selectDir(_dir) - return - } else if (_dir) { - currDirs = _dir.dirs - } + let previousPath = '' + let lookupPath = '' + + if (splitPaths.length > 2) { + lookupPath = splitPaths.slice(0, -2).join('/') } + previousPath = splitPaths.slice(0, -1).join('/') + + if (!this.isPosix) { + // For windows drives add a trailing slash. e.g. C:/ + if (!this.isPosix && lookupPath.endsWith(':')) { + lookupPath += '/' + } + if (!this.isPosix && previousPath.endsWith(':')) { + previousPath += '/' + } + } else { + // Add leading slash + if (previousPath) previousPath = '/' + previousPath + if (lookupPath) lookupPath = '/' + lookupPath + } + + this.level-- + this.subdirs = this.directories + this.selectedPath = previousPath + this.directories = await this.fetchDirs(lookupPath, this.level) }, - selectDir(dir) { + async selectDir(dir) { if (dir.isUsed) return this.selectedPath = dir.path - this.selectedFullPath = dir.fullPath this.level = dir.level - this.subdirs = dir.dirs + this.subdirs = await this.fetchDirs(dir.path, dir.level + 1) }, - selectSubDir(dir) { + async selectSubDir(dir) { if (dir.isUsed) return this.selectedPath = dir.path - this.selectedFullPath = dir.fullPath this.level = dir.level this.directories = this.subdirs - this.subdirs = dir.dirs + this.subdirs = await this.fetchDirs(dir.path, dir.level + 1) }, selectFolder() { if (!this.selectedPath) { console.error('No Selected path') return } - if (this.paths.find((p) => p.startsWith(this.selectedFullPath))) { + if (this.paths.find((p) => p.startsWith(this.selectedPath))) { this.$toast.error(`Oops, you cannot add a parent directory of a folder already added`) return } - this.$emit('select', this.selectedFullPath) + this.$emit('select', this.selectedPath) this.selectedPath = '' - this.selectedFullPath = '' + }, + fetchDirs(path, level) { + this.loadingDirs = true + return this.$axios + .$get(`/api/filesystem?path=${path}&level=${level}`) + .then((data) => { + console.log('Fetched directories', data.directories) + this.isPosix = !!data.posix + return data.directories + }) + .catch((error) => { + console.error('Failed to get filesystem paths', error) + this.$toast.error('Failed to get filesystem paths') + return [] + }) + .finally(() => { + this.loadingDirs = false + }) }, async init() { - this.loadingFolders = true - this.allFolders = await this.$store.dispatch('libraries/loadFolders') - this.loadingFolders = false + this.initialLoad = true + this.rootDirs = await this.fetchDirs('', 0) + this.initialLoad = false - this.directories = this.allFolders + this.directories = this.rootDirs this.subdirs = [] this.selectedPath = '' - this.selectedFullPath = '' } }, mounted() { diff --git a/server/controllers/FileSystemController.js b/server/controllers/FileSystemController.js index cee52cb2..88459e51 100644 --- a/server/controllers/FileSystemController.js +++ b/server/controllers/FileSystemController.js @@ -1,31 +1,69 @@ const Path = require('path') const Logger = require('../Logger') -const Database = require('../Database') const fs = require('../libs/fsExtra') +const { toNumber } = require('../utils/index') +const fileUtils = require('../utils/fileUtils') class FileSystemController { constructor() { } + /** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ async getPaths(req, res) { if (!req.user.isAdminOrUp) { Logger.error(`[FileSystemController] Non-admin user attempting to get filesystem paths`, req.user) return res.sendStatus(403) } - const excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc'].map(dirname => { - return Path.sep + dirname - }) + const relpath = req.query.path + const level = toNumber(req.query.level, 0) - // Do not include existing mapped library paths in response - const libraryFoldersPaths = await Database.libraryFolderModel.getAllLibraryFolderPaths() - libraryFoldersPaths.forEach((path) => { - let dir = path || '' - if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '') - excludedDirs.push(dir) + // Validate path. Must be absolute + if (relpath && (!Path.isAbsolute(relpath) || !await fs.pathExists(relpath))) { + Logger.error(`[FileSystemController] Invalid path in query string "${relpath}"`) + return res.status(400).send('Invalid "path" query string') + } + Logger.debug(`[FileSystemController] Getting file paths at ${relpath || 'root'} (${level})`) + + let directories = [] + + // Windows returns drives first + if (global.isWin) { + if (relpath) { + directories = await fileUtils.getDirectoriesInPath(relpath, level) + } else { + const drives = await fileUtils.getWindowsDrives().catch((error) => { + Logger.error(`[FileSystemController] Failed to get windows drives`, error) + return [] + }) + if (drives.length) { + directories = drives.map(d => { + return { + path: d, + dirname: d, + level: 0 + } + }) + } + } + } else { + directories = await fileUtils.getDirectoriesInPath(relpath || '/', level) + } + + // Exclude some dirs from this project to be cleaner in Docker + const excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc', '.devcontainer', '.nyc_output', '.github', '.vscode'].map(dirname => { + return fileUtils.filePathToPOSIX(Path.join(global.appRoot, dirname)) + }) + directories = directories.filter(dir => { + return !excludedDirs.includes(dir.path) }) res.json({ - directories: await this.getDirectories(global.appRoot, '/', excludedDirs) + posix: !global.isWin, + directories }) } diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 3edce256..2956cd52 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -320,35 +320,6 @@ class ApiRouter { this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this)) } - async getDirectories(dir, relpath, excludedDirs, level = 0) { - try { - const paths = await fs.readdir(dir) - - let dirs = await Promise.all(paths.map(async dirname => { - const fullPath = Path.join(dir, dirname) - const path = Path.join(relpath, dirname) - - const isDir = (await fs.lstat(fullPath)).isDirectory() - if (isDir && !excludedDirs.includes(path) && dirname !== 'node_modules') { - return { - path, - dirname, - fullPath, - level, - dirs: level < 4 ? (await this.getDirectories(fullPath, path, excludedDirs, level + 1)) : [] - } - } else { - return false - } - })) - dirs = dirs.filter(d => d) - return dirs - } catch (error) { - Logger.error('Failed to readdir', dir, error) - return [] - } - } - // // Helper Methods // diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 89ad9e60..14e4d743 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -1,6 +1,7 @@ const axios = require('axios') const Path = require('path') const ssrfFilter = require('ssrf-req-filter') +const exec = require('child_process').exec const fs = require('../libs/fsExtra') const rra = require('../libs/recursiveReaddirAsync') const Logger = require('../Logger') @@ -378,3 +379,65 @@ module.exports.isWritable = async (directory) => { } } +/** + * Get Windows drives as array e.g. ["C:/", "F:/"] + * + * @returns {Promise<string[]>} + */ +module.exports.getWindowsDrives = async () => { + if (!global.isWin) { + return [] + } + return new Promise((resolve, reject) => { + exec('wmic logicaldisk get name', async (error, stdout, stderr) => { + if (error) { + reject(error) + return + } + let drives = stdout?.split(/\r?\n/).map(line => line.trim()).filter(line => line).slice(1) + const validDrives = [] + for (const drive of drives) { + let drivepath = drive + '/' + if (await fs.pathExists(drivepath)) { + validDrives.push(drivepath) + } else { + Logger.error(`Invalid drive ${drivepath}`) + } + } + resolve(validDrives) + }) + }) +} + +/** + * Get array of directory paths in a directory + * + * @param {string} dirPath + * @param {number} level + * @returns {Promise<{ path:string, dirname:string, level:number }[]>} + */ +module.exports.getDirectoriesInPath = async (dirPath, level) => { + try { + const paths = await fs.readdir(dirPath) + let dirs = await Promise.all(paths.map(async dirname => { + const fullPath = Path.join(dirPath, dirname) + + const lstat = await fs.lstat(fullPath).catch((error) => { + Logger.debug(`Failed to lstat "${fullPath}"`, error) + return null + }) + if (!lstat?.isDirectory()) return null + + return { + path: this.filePathToPOSIX(fullPath), + dirname, + level + } + })) + dirs = dirs.filter(d => d) + return dirs + } catch (error) { + Logger.error('Failed to readdir', dirPath, error) + return [] + } +} \ No newline at end of file From 56eff7a236b0d763024930d7eac39f30355d7a5a Mon Sep 17 00:00:00 2001 From: mozhu <lcl_em@163.com> Date: Thu, 4 Jan 2024 11:52:45 +0800 Subject: [PATCH 0288/2145] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=92=AD=E5=AE=A2?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=9C=B0=E5=8C=BA=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/pages/config/index.vue | 4 ++++ client/plugins/i18n.js | 10 ++++++++++ client/strings/cs.json | 1 + 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/hr.json | 1 + client/strings/it.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/sv.json | 1 + client/strings/zh-cn.json | 1 + server/objects/settings/ServerSettings.js | 3 +++ server/providers/iTunes.js | 3 ++- 21 files changed, 36 insertions(+), 1 deletion(-) diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 12ce7b1e..8cd33b0e 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -135,6 +135,10 @@ <ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" /> </div> + <div class="py-2"> + <ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="newServerSettings.language" :items="$podcastSearchRegionOptions" small class="max-w-52" @input="(val) => updateSettingsKey('podcastSearchRegion', val)" /> + </div> + <!-- old experimental features --> <!-- <div class="pt-4"> <h2 class="font-semibold">{{ $strings.HeaderSettingsExperimental }}</h2> diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index ea6a06db..bb719bf7 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -27,6 +27,16 @@ Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { value: code } }) +const podcastSearchRegionMap = { + 'us': { label: 'United States' }, + 'cn': { label: '中国' }, +} +Vue.prototype.$podcastSearchRegionOptions = Object.keys(podcastSearchRegionMap).map(code => { + return { + text: podcastSearchRegionMap[code].label, + value: code + } +}) Vue.prototype.$languageCodes = { default: defaultCode, diff --git a/client/strings/cs.json b/client/strings/cs.json index bac376d8..a7328e60 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Vaše záložky", "LabelYourPlaylists": "Vaše seznamy přehrávání", "LabelYourProgress": "Váš pokrok", + "LabelPodcastSearchRegion": "Oblast vyhledávání podcastu", "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.", diff --git a/client/strings/da.json b/client/strings/da.json index 3dd611d9..ce0c58bc 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Dine bogmærker", "LabelYourPlaylists": "Dine spillelister", "LabelYourProgress": "Din fremgang", + "LabelPodcastSearchRegion": "Podcast søgeområde", "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.", diff --git a/client/strings/de.json b/client/strings/de.json index 20da77c1..d8ad7768 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Lesezeichen", "LabelYourPlaylists": "Eigene Wiedergabelisten", "LabelYourProgress": "Fortschritt", + "LabelPodcastSearchRegion": "Podcast-Suchregion", "MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen", "MessageAppriseDescription": "Um diese Funktion nutzen zu können, müssen Sie eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würden Sie <code>http://192.168.1.1:8337/notify</code> eingeben.", "MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index f69175fd..667218ec 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Your Bookmarks", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Your Progress", + "LabelPodcastSearchRegion": "Podcast search region", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.", diff --git a/client/strings/es.json b/client/strings/es.json index cefeb8f8..717c63e5 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Tus Marcadores", "LabelYourPlaylists": "Tus Listas", "LabelYourProgress": "Tu Progreso", + "LabelPodcastSearchRegion": "Región de búsqueda de podcasts", "MessageAddToPlayerQueue": "Agregar a fila del Reproductor", "MessageAppriseDescription": "Para usar esta función deberás tener <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">la API de Apprise</a> corriendo o una API que maneje los mismos resultados. <br/>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 <code>http://192.168.1.1:8337</code> entonces pondría <code>http://192.168.1.1:8337/notify</code>.", "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 <code>/metadata/items</code> y <code>/metadata/authors</code>. Los Respaldos <strong>NO</strong> incluyen ningún archivo guardado en la carpeta de tu biblioteca.", diff --git a/client/strings/fr.json b/client/strings/fr.json index 86a64602..65a78554 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Vos signets", "LabelYourPlaylists": "Vos listes de lecture", "LabelYourProgress": "Votre progression", + "LabelPodcastSearchRegion": "Région de recherche de podcasts", "MessageAddToPlayerQueue": "Ajouter en file d’attente", "MessageAppriseDescription": "Nécessite une instance d’<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes. <br />l’URL de l’API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les sauvegardes n’incluent pas les fichiers de votre bibliothèque.", diff --git a/client/strings/gu.json b/client/strings/gu.json index 24c874eb..cad45e97 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Your Bookmarks", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Your Progress", + "LabelPodcastSearchRegion": "પોડકાસ્ટ શોધ પ્રદેશ", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.", diff --git a/client/strings/hi.json b/client/strings/hi.json index e7ec6155..22af053d 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Your Bookmarks", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Your Progress", + "LabelPodcastSearchRegion": "पॉडकास्ट खोज क्षेत्र", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.", diff --git a/client/strings/hr.json b/client/strings/hr.json index edefcf53..8399db56 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Tvoje knjižne oznake", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Tvoj napredak", + "LabelPodcastSearchRegion": "Područje pretrage podcasta", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Backups uključuju korisnike, korisnikov napredak, detalje stavki iz biblioteke, postavke server i slike iz <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups ne uključuju nijedne datoteke koje su u folderima biblioteke.", diff --git a/client/strings/it.json b/client/strings/it.json index 0860e83f..1718724a 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "I tuoi Preferiti", "LabelYourPlaylists": "le tue Playlist", "LabelYourProgress": "Completato al", + "LabelPodcastSearchRegion": "Area di ricerca podcast", "MessageAddToPlayerQueue": "Aggiungi alla coda di riproduzione", "MessageAppriseDescription": "Per utilizzare questa funzione è necessario disporre di un'istanza di <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> in esecuzione o un'API che gestirà quelle stesse richieste. <br />L'API Url dovrebbe essere il percorso URL completo per inviare la notifica, ad esempio se la tua istanza API è servita cosi .<code>http://192.168.1.1:8337</code> Allora dovrai mettere <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "I backup includono utenti, progressi degli utenti, dettagli sugli elementi della libreria, impostazioni del server e immagini archiviate in <code>/metadata/items</code> & <code>/metadata/authors</code>. I backup non includono i file archiviati nelle cartelle della libreria.", diff --git a/client/strings/lt.json b/client/strings/lt.json index 94067198..04b820d1 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Jūsų skirtukai", "LabelYourPlaylists": "Jūsų grojaraščiai", "LabelYourProgress": "Jūsų pažanga", + "LabelPodcastSearchRegion": "Podcast paieškos regionas", "MessageAddToPlayerQueue": "Pridėti į grotuvo eilę", "MessageAppriseDescription": "Norint naudoti šią funkciją, reikės turėti <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> veikiantį arba API, kuris tvarkys tas pačias užklausas.<br />Apprise API URL turėtų būti visi kelio takai iki pranešimo siuntimo, pvz., jei jūsų API pasiekiamas adresu <code>http://192.168.1.1:8337</code>, tada įveskite <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Atsarginės kopijos apima vartotojus, vartotojų pažangą, bibliotekos elemento informaciją, serverio nustatymus ir vaizdus, saugomus <code>/metadata/items</code> ir <code>/metadata/authors</code>. Atsarginės kopijos <strong>neįtraukia</strong> jokių failų, saugomų jūsų bibliotekos aplankuose.", diff --git a/client/strings/nl.json b/client/strings/nl.json index 19a5c35a..d7fba348 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Je boekwijzers", "LabelYourPlaylists": "Je afspeellijsten", "LabelYourProgress": "Je voortgang", + "LabelPodcastSearchRegion": "Podcast zoekregio", "MessageAddToPlayerQueue": "Toevoegen aan wachtrij", "MessageAppriseDescription": "Om deze functie te gebruiken heb je een draaiende instantie van <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nodig of een api die dezelfde requests afhandelt. <br />De Apprise API Url moet het volledige URL-pad zijn om de notificatie te verzenden, b.v., als je API-instantie draait op <code>http://192.168.1.1:8337</code> dan zou je <code>http://192.168.1.1:8337/notify</code> gebruiken.", "MessageBackupsDescription": "Back-ups omvatten gebruikers, gebruikers' voortgang, bibliotheekonderdeeldetails, serverinstellingen en afbeeldingen bewaard in <code>/metadata/items</code> & <code>/metadata/authors</code>. Back-ups <strong>bevatten niet</strong> de bestanden bewaard in je bibliotheekmappen.", diff --git a/client/strings/no.json b/client/strings/no.json index 37cbc7a8..d8102863 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Dine bokmerker", "LabelYourPlaylists": "Dine spillelister", "LabelYourProgress": "Din fremgang", + "LabelPodcastSearchRegion": "Podcast-søkeområde", "MessageAddToPlayerQueue": "Legg til i kø", "MessageAppriseDescription": "For å bruke denne funksjonen trenger du en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> kjørende eller ett api som vil håndere disse forespørslene. <br />Apprise API Url skal være den fulle URL stien for å sende Notifikasjonen, f.eks., hvis din API instans er hos <code>http://192.168.1.1:8337</code> vil du bruke <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Sikkerhetskopier inkluderer, brukerfremgang, detaljer om bibliotekgjenstander, tjener instillinger og bilder lagret under <code>/metadata/items</code> og <code>/metadata/authors</code>. Sikkerhetskopier <strong>vil ikke</strong> inkludere filer som er lagret i bibliotek mappene.", diff --git a/client/strings/pl.json b/client/strings/pl.json index 86e29274..5c069787 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Twoje zakładki", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Twój postęp", + "LabelPodcastSearchRegion": "Obszar wyszukiwania podcastów", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "Aby użyć tej funkcji, konieczne jest posiadanie instancji <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> albo innego rozwiązania, które obsługuje schemat zapytań Apprise. <br />URL do interfejsu API powinno być całkowitą ścieżką, np., jeśli Twoje API do powiadomień jest dostępne pod adresem <code>http://192.168.1.1:8337</code> to wpisany tutaj URL powinien mieć postać: <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Kopie zapasowe obejmują użytkowników, postępy użytkowników, szczegóły pozycji biblioteki, ustawienia serwera i obrazy przechowywane w <code>/metadata/items</code> & <code>/metadata/authors</code>. Kopie zapasowe nie obejmują żadnych plików przechowywanych w folderach biblioteki.", diff --git a/client/strings/ru.json b/client/strings/ru.json index 4d26c4aa..ac961b7f 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Ваши закладки", "LabelYourPlaylists": "Ваши плейлисты", "LabelYourProgress": "Ваш прогресс", + "LabelPodcastSearchRegion": "Регион поиска подкастов", "MessageAddToPlayerQueue": "Добавить в очередь проигрывателя", "MessageAppriseDescription": "Для использования этой функции необходимо иметь запущенный экземпляр <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или api которое обрабатывает те же самые запросы. <br />URL-адрес API Apprise должен быть полным URL-адресом для отправки уведомления, т.е., если API запущено по адресу <code>http://192.168.1.1:8337</code> тогда нужно указать <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Бэкап включает пользователей, прогресс пользователей, данные элементов библиотеки, настройки сервера и изображения хранящиеся в <code>/metadata/items</code> и <code>/metadata/authors</code>. Бэкапы <strong>НЕ</strong> сохраняют файлы из папок библиотек.", diff --git a/client/strings/sv.json b/client/strings/sv.json index 1d71fce5..b01d9480 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "Dina bokmärken", "LabelYourPlaylists": "Dina spellistor", "LabelYourProgress": "Din framsteg", + "LabelPodcastSearchRegion": "Podcast-sökområde", "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.", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 05553c08..61e3b4a7 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -540,6 +540,7 @@ "LabelYourBookmarks": "你的书签", "LabelYourPlaylists": "你的播放列表", "LabelYourProgress": "你的进度", + "LabelPodcastSearchRegion": "播客搜索地区", "MessageAddToPlayerQueue": "添加到播放队列", "MessageAppriseDescription": "要使用此功能,您需要运行一个 <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> 实例或一个可以处理这些相同请求的 API. <br />Apprise API Url 应该是发送通知的完整 URL 路径, 例如: 如果你的 API 实例运行在 <code>http://192.168.1.1:8337</code>, 那么你可以输入 <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "备份包括用户, 用户进度, 媒体库项目详细信息, 服务器设置和图像, 存储在 <code>/metadata/items</code> & <code>/metadata/authors</code>. 备份不包括存储在您的媒体库文件夹中的任何文件.", diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 6e9d8456..2a788dff 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -48,6 +48,7 @@ class ServerSettings { this.dateFormat = 'MM/dd/yyyy' this.timeFormat = 'HH:mm' this.language = 'en-us' + this.podcastSearchRegion = 'us' this.logLevel = Logger.logLevel @@ -109,6 +110,7 @@ class ServerSettings { this.dateFormat = settings.dateFormat || 'MM/dd/yyyy' this.timeFormat = settings.timeFormat || 'HH:mm' this.language = settings.language || 'en-us' + this.podcastSearchRegion = settings.podcastSearchRegion || 'us' this.logLevel = settings.logLevel || Logger.logLevel this.version = settings.version || null this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 @@ -198,6 +200,7 @@ class ServerSettings { dateFormat: this.dateFormat, timeFormat: this.timeFormat, language: this.language, + podcastSearchRegion: this.podcastSearchRegion, logLevel: this.logLevel, version: this.version, buildNumber: this.buildNumber, diff --git a/server/providers/iTunes.js b/server/providers/iTunes.js index 39f36ab2..a857a201 100644 --- a/server/providers/iTunes.js +++ b/server/providers/iTunes.js @@ -1,6 +1,7 @@ const axios = require('axios') const Logger = require('../Logger') const htmlSanitizer = require('../utils/htmlSanitizer') +const Database = require('../Database') class iTunes { constructor() { } @@ -17,7 +18,7 @@ class iTunes { entity: options.entity, lang: options.lang, limit: options.limit, - country: options.country + country: options.country ? options.country : Database.serverSettings.podcastSearchRegion } return axios.get('https://itunes.apple.com/search', { params: query }).then((response) => { return response.data.results || [] From 1be34564f29d88298bd1a06c078fbf78a9281ad3 Mon Sep 17 00:00:00 2001 From: mozhu <lcl_em@163.com> Date: Thu, 4 Jan 2024 15:00:40 +0800 Subject: [PATCH 0289/2145] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/pages/config/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 8cd33b0e..ccdbe2e4 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -136,7 +136,7 @@ </div> <div class="py-2"> - <ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="newServerSettings.language" :items="$podcastSearchRegionOptions" small class="max-w-52" @input="(val) => updateSettingsKey('podcastSearchRegion', val)" /> + <ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="newServerSettings.podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-52" @input="(val) => updateSettingsKey('podcastSearchRegion', val)" /> </div> <!-- old experimental features --> From ffa7cc0d22dbe42457745083b8cdafaa0c93640c Mon Sep 17 00:00:00 2001 From: Machou <Machou@users.noreply.github.com> Date: Fri, 5 Jan 2024 07:19:07 +0100 Subject: [PATCH 0290/2145] Update fr.json --- client/strings/fr.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index 3db4b43a..e570614d 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -101,7 +101,7 @@ "HeaderChapters": "Chapitres", "HeaderChooseAFolder": "Choisir un dossier", "HeaderCollection": "Collection", - "HeaderCollectionItems": "Entrées de la Collection", + "HeaderCollectionItems": "Entrées de la collection", "HeaderCover": "Couverture", "HeaderCurrentDownloads": "Téléchargements en cours", "HeaderDetails": "Détails", @@ -114,10 +114,10 @@ "HeaderEreaderSettings": "Options Ereader", "HeaderFiles": "Fichiers", "HeaderFindChapters": "Trouver les chapitres", - "HeaderIgnoredFiles": "Fichiers Ignorés", - "HeaderItemFiles": "Fichiers des Articles", + "HeaderIgnoredFiles": "Fichiers ignorés", + "HeaderItemFiles": "Fichiers des articles", "HeaderItemMetadataUtils": "Outils de gestion des métadonnées", - "HeaderLastListeningSession": "Dernière Session d’écoute", + "HeaderLastListeningSession": "Dernière session d’écoute", "HeaderLatestEpisodes": "Dernier épisodes", "HeaderLibraries": "Bibliothèque", "HeaderLibraryFiles": "Fichier de bibliothèque", @@ -239,7 +239,7 @@ "LabelCurrent": "Actuel", "LabelCurrently": "Actuellement :", "LabelCustomCronExpression": "Expression cron personnalisée :", - "LabelDatetime": "Datetime", // need review with context + "LabelDatetime": "Date", "LabelDeleteFromFileSystemCheckbox": "Supprimer du système de fichiers (décocher pour ne supprimer que de la base de données)", "LabelDescription": "Description", "LabelDeselectAll": "Tout déselectionner", @@ -247,8 +247,8 @@ "LabelDeviceInfo": "Détail de l’appareil", "LabelDeviceIsAvailableTo": "L’appareil est disponible pour…", "LabelDirectory": "Répertoire", - "LabelDiscFromFilename": "Disque à partir du fichier", // need review with context - "LabelDiscFromMetadata": "Disque à partir des métadonnées", // need review with context + "LabelDiscFromFilename": "Depuis le fichier", + "LabelDiscFromMetadata": "Depuis les métadonnées", "LabelDiscover": "Découvrir", "LabelDownload": "Téléchargement", "LabelDownloadNEpisodes": "Télécharger {0} épisode(s)", @@ -278,7 +278,7 @@ "LabelFilename": "Nom de fichier", "LabelFilterByUser": "Filtrer par utilisateur", "LabelFindEpisodes": "Trouver des épisodes", - "LabelFinished": "Terminé", // need review with context + "LabelFinished": "Terminé le", "LabelFolder": "Dossier", "LabelFolders": "Dossiers", "LabelFontFamily": "Polices de caractères", @@ -406,7 +406,7 @@ "LabelRegion": "Région", "LabelReleaseDate": "Date de parution", "LabelRemoveCover": "Supprimer la couverture", - "LabelRowsPerPage": "Rows per page", + "LabelRowsPerPage": "Lignes par page", "LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé", "LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé", "LabelRSSFeedOpen": "Flux RSS ouvert", @@ -428,18 +428,18 @@ "LabelSetEbookAsPrimary": "Définir comme principale", "LabelSetEbookAsSupplementary": "Définir comme supplémentaire", "LabelSettingsAudiobooksOnly": "Livres audios seulement", - "LabelSettingsAudiobooksOnlyHelp": "L’activation de ce paramètre ignorera les fichiers “ ebook ”, à moins qu’ils ne se trouvent dans un dossier de livres audio, auquel cas ils seront définis comme des livres numériques supplémentaires.", + "LabelSettingsAudiobooksOnlyHelp": "L'activation de ce paramètre ignorera les fichiers de type « livre numériques », sauf s'ils se trouvent dans un dossier spécifique , auquel cas ils seront définis comme des livres numériques supplémentaires.", "LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois", "LabelSettingsChromecastSupport": "Support du Chromecast", "LabelSettingsDateFormat": "Format de date", "LabelSettingsDisableWatcher": "Désactiver la surveillance", "LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance des dossiers pour la bibliothèque", - "LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque des modifications de fichiers sont détectées. *Nécessite le redémarrage du serveur", + "LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque des modifications de fichiers sont détectées. * nécessite le redémarrage du serveur", "LabelSettingsEnableWatcher": "Activer la veille", "LabelSettingsEnableWatcherForLibrary": "Activer la surveillance des dossiers pour la bibliothèque", - "LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique automatique lorsque des modifications de fichiers sont détectées. *Nécessite le redémarrage du serveur", + "LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique automatique lorsque des modifications de fichiers sont détectées. * nécessite le redémarrage du serveur", "LabelSettingsExperimentalFeatures": "Fonctionnalités expérimentales", - "LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion Github.", + "LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion GitHub.", "LabelSettingsFindCovers": "Chercher des couvertures de livre", "LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, l’analyseur tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps d’analyse.", "LabelSettingsHideSingleBookSeries": "Masquer les séries de livres uniques", @@ -503,7 +503,7 @@ "LabelToolsEmbedMetadata": "Métadonnées intégrées", "LabelToolsEmbedMetadataDescription": "Intègre les métadonnées au fichier audio avec la couverture et les chapitres.", "LabelToolsMakeM4b": "Créer un fichier livre audio M4B", - "LabelToolsMakeM4bDescription": "Génère un fichier livre audio .M4B avec intégration des métadonnées, image de couverture et les chapitres.", + "LabelToolsMakeM4bDescription": "Générer un fichier de livre audio .M4B avec des métadonnées intégrées, une image de couverture et des chapitres.", "LabelToolsSplitM4b": "Scinde le fichier M4B en fichiers MP3", "LabelToolsSplitM4bDescription": "Créer plusieurs fichier MP3 à partir du découpage par chapitre, en incluant les métadonnées, l’image de couverture et les chapitres.", "LabelTotalDuration": "Durée totale", @@ -541,7 +541,7 @@ "LabelYourPlaylists": "Vos listes de lecture", "LabelYourProgress": "Votre progression", "MessageAddToPlayerQueue": "Ajouter en file d’attente", - "MessageAppriseDescription": "Nécessite une instance d’<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes. <br />l’URL de l’API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.", + "MessageAppriseDescription": "Nécessite une instance d’<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes.<br>L’URL de l’API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les sauvegardes n’incluent pas les fichiers de votre bibliothèque.", "MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer l’option suivante pour autoriser la recherche par correspondance à écraser les données existantes.", "MessageBookshelfNoCollections": "Vous n’avez pas encore de collections", @@ -650,7 +650,7 @@ "MessageReportBugsAndContribute": "Remonter des anomalies, demander des fonctionnalités et contribuer sur", "MessageResetChaptersConfirm": "Êtes-vous sûr de vouloir réinitialiser les chapitres et annuler les changements effectués ?", "MessageRestoreBackupConfirm": "Êtes-vous sûr de vouloir restaurer la sauvegarde créée le", - "MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.<br /><br />Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br /><br />Tous les clients utilisant votre serveur seront automatiquement mis à jour.", + "MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.<br><br>Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br><br>Tous les clients utilisant votre serveur seront automatiquement mis à jour.", "MessageSearchResultsFor": "Résultats de recherche pour", "MessageSelected": "{0} sélectionnés", "MessageServerCouldNotBeReached": "Serveur inaccessible", From fea78898a5de723b1790b5f640f8202dc74dc3b4 Mon Sep 17 00:00:00 2001 From: mozhu <lcl_em@163.com> Date: Fri, 5 Jan 2024 14:45:35 +0800 Subject: [PATCH 0291/2145] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E6=92=AD=E5=AE=A2?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=9C=B0=E5=8C=BA=E9=85=8D=E7=BD=AE=E5=88=B0?= =?UTF-8?q?=E5=AA=92=E4=BD=93=E5=BA=93=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modals/libraries/LibrarySettings.vue | 15 ++++++++++++--- client/pages/config/index.vue | 4 ---- client/pages/library/_library/podcast/search.vue | 5 ++++- server/controllers/SearchController.js | 5 ++++- server/objects/settings/LibrarySettings.js | 5 ++++- server/objects/settings/ServerSettings.js | 3 --- server/providers/iTunes.js | 3 +-- 7 files changed, 25 insertions(+), 15 deletions(-) diff --git a/client/components/modals/libraries/LibrarySettings.vue b/client/components/modals/libraries/LibrarySettings.vue index 53eb2650..c62f769b 100644 --- a/client/components/modals/libraries/LibrarySettings.vue +++ b/client/components/modals/libraries/LibrarySettings.vue @@ -49,6 +49,9 @@ </ui-tooltip> </div> </div> + <div v-if="isPodcastLibrary" class="py-3"> + <ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-52" @input="formUpdated" /> + </div> </div> </template> @@ -69,7 +72,8 @@ export default { skipMatchingMediaWithAsin: false, skipMatchingMediaWithIsbn: false, audiobooksOnly: false, - hideSingleBookSeries: false + hideSingleBookSeries: false, + podcastSearchRegion: 'us' } }, computed: { @@ -85,6 +89,9 @@ export default { isBookLibrary() { return this.mediaType === 'book' }, + isPodcastLibrary() { + return this.mediaType === 'podcast' + }, providers() { if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders return this.$store.state.scanners.providers @@ -99,7 +106,8 @@ export default { skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin, skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn, audiobooksOnly: !!this.audiobooksOnly, - hideSingleBookSeries: !!this.hideSingleBookSeries + hideSingleBookSeries: !!this.hideSingleBookSeries, + podcastSearchRegion: this.podcastSearchRegion } } }, @@ -112,7 +120,8 @@ export default { this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn this.audiobooksOnly = !!this.librarySettings.audiobooksOnly - this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries + this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries, + this.podcastSearchRegion = this.librarySettings.podcastSearchRegion } }, mounted() { diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index ccdbe2e4..12ce7b1e 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -135,10 +135,6 @@ <ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" /> </div> - <div class="py-2"> - <ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="newServerSettings.podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-52" @input="(val) => updateSettingsKey('podcastSearchRegion', val)" /> - </div> - <!-- old experimental features --> <!-- <div class="pt-4"> <h2 class="font-semibold">{{ $strings.HeaderSettingsExperimental }}</h2> diff --git a/client/pages/library/_library/podcast/search.vue b/client/pages/library/_library/podcast/search.vue index 3be851ce..e786562c 100644 --- a/client/pages/library/_library/podcast/search.vue +++ b/client/pages/library/_library/podcast/search.vue @@ -86,6 +86,9 @@ export default { }, streamLibraryItem() { return this.$store.state.streamLibraryItem + }, + librarySetting() { + return this.$store.getters['libraries/getCurrentLibrarySettings'] } }, methods: { @@ -151,7 +154,7 @@ export default { async submitSearch(term) { this.processing = true this.termSearched = '' - let results = await this.$axios.$get(`/api/search/podcast?term=${encodeURIComponent(term)}`).catch((error) => { + let results = await this.$axios.$get(`/api/search/podcast?term=${encodeURIComponent(term)}&country=${encodeURIComponent(this.librarySetting?.podcastSearchRegion)}`).catch((error) => { console.error('Search request failed', error) return [] }) diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index 34d65e3c..1acbe926 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -43,12 +43,15 @@ class SearchController { */ async findPodcasts(req, res) { const term = req.query.term + const country = req.query.country if (!term) { Logger.error('[SearchController] Invalid request query param "term" is required') return res.status(400).send('Invalid request query param "term" is required') } - const results = await PodcastFinder.search(term) + const results = await PodcastFinder.search(term, { + country: country + }) res.json(results) } diff --git a/server/objects/settings/LibrarySettings.js b/server/objects/settings/LibrarySettings.js index 10ee19e0..b070ff79 100644 --- a/server/objects/settings/LibrarySettings.js +++ b/server/objects/settings/LibrarySettings.js @@ -10,6 +10,7 @@ class LibrarySettings { this.audiobooksOnly = false this.hideSingleBookSeries = false // Do not show series that only have 1 book this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] + this.podcastSearchRegion = 'us' if (settings) { this.construct(settings) @@ -30,6 +31,7 @@ class LibrarySettings { // Added in v2.4.5 this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] } + this.podcastSearchRegion = settings.podcastSearchRegion || 'us' } toJSON() { @@ -41,7 +43,8 @@ class LibrarySettings { autoScanCronExpression: this.autoScanCronExpression, audiobooksOnly: this.audiobooksOnly, hideSingleBookSeries: this.hideSingleBookSeries, - metadataPrecedence: [...this.metadataPrecedence] + metadataPrecedence: [...this.metadataPrecedence], + podcastSearchRegion: this.podcastSearchRegion } } diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 2a788dff..6e9d8456 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -48,7 +48,6 @@ class ServerSettings { this.dateFormat = 'MM/dd/yyyy' this.timeFormat = 'HH:mm' this.language = 'en-us' - this.podcastSearchRegion = 'us' this.logLevel = Logger.logLevel @@ -110,7 +109,6 @@ class ServerSettings { this.dateFormat = settings.dateFormat || 'MM/dd/yyyy' this.timeFormat = settings.timeFormat || 'HH:mm' this.language = settings.language || 'en-us' - this.podcastSearchRegion = settings.podcastSearchRegion || 'us' this.logLevel = settings.logLevel || Logger.logLevel this.version = settings.version || null this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 @@ -200,7 +198,6 @@ class ServerSettings { dateFormat: this.dateFormat, timeFormat: this.timeFormat, language: this.language, - podcastSearchRegion: this.podcastSearchRegion, logLevel: this.logLevel, version: this.version, buildNumber: this.buildNumber, diff --git a/server/providers/iTunes.js b/server/providers/iTunes.js index a857a201..39f36ab2 100644 --- a/server/providers/iTunes.js +++ b/server/providers/iTunes.js @@ -1,7 +1,6 @@ const axios = require('axios') const Logger = require('../Logger') const htmlSanitizer = require('../utils/htmlSanitizer') -const Database = require('../Database') class iTunes { constructor() { } @@ -18,7 +17,7 @@ class iTunes { entity: options.entity, lang: options.lang, limit: options.limit, - country: options.country ? options.country : Database.serverSettings.podcastSearchRegion + country: options.country } return axios.get('https://itunes.apple.com/search', { params: query }).then((response) => { return response.data.results || [] From 81020ff34daf4cec48d3ca7ddad520f3921d8b31 Mon Sep 17 00:00:00 2001 From: mozhu <lcl_em@163.com> Date: Fri, 5 Jan 2024 15:50:20 +0800 Subject: [PATCH 0292/2145] =?UTF-8?q?=E6=92=AD=E5=AE=A2=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=9C=B0=E5=8C=BA=E9=85=8D=E7=BD=AE=E5=A2=9E=E5=8A=A0=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/components/modals/libraries/LibrarySettings.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/modals/libraries/LibrarySettings.vue b/client/components/modals/libraries/LibrarySettings.vue index c62f769b..4712d6a2 100644 --- a/client/components/modals/libraries/LibrarySettings.vue +++ b/client/components/modals/libraries/LibrarySettings.vue @@ -120,8 +120,8 @@ export default { this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn this.audiobooksOnly = !!this.librarySettings.audiobooksOnly - this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries, - this.podcastSearchRegion = this.librarySettings.podcastSearchRegion + this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries + this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us' } }, mounted() { From 578a59063f1839cfd7b14f023d3c90c254bfb08a Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 5 Jan 2024 09:24:18 -0600 Subject: [PATCH 0293/2145] Update discord invite link --- .github/ISSUE_TEMPLATE/bug.yaml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- client/pages/config/index.vue | 4 ++-- readme.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 1d422810..ca044b71 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -11,7 +11,7 @@ body: value: "### Mobile app issues report [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)." - type: markdown attributes: - value: "### Join the [discord server](https://discord.gg/pJsjuNCKRq) for questions or if you are not sure about a bug." + value: "### Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug." - type: markdown attributes: value: "## Be as descriptive as you can. Include screenshots, error logs, browser, file types, everything you can think of that might be relevant." diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 63cb8805..2c6cc191 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: Discord - url: https://discord.gg/pJsjuNCKRq + url: https://discord.gg/HQgCbd6E75 about: Ask questions, get help troubleshooting, and join the Abs community here. - name: Matrix url: https://matrix.to/#/#audiobookshelf:matrix.org diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 12ce7b1e..acc92ea5 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -178,9 +178,9 @@ </a> <p class="pl-4 pr-2 text-sm text-yellow-400"> {{ $strings.MessageJoinUsOn }} - <a class="underline" href="https://discord.gg/pJsjuNCKRq" target="_blank">discord</a> + <a class="underline" href="https://discord.gg/HQgCbd6E75" target="_blank">discord</a> </p> - <a href="https://discord.gg/pJsjuNCKRq" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500"> + <a href="https://discord.gg/HQgCbd6E75" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500"> <svg width="31" height="24" viewBox="0 0 71 55" fill="none" xmlns="http://www.w3.org/2000/svg"> <g clip-path="url(#clip0)"> <path diff --git a/readme.md b/readme.md index 3ebe097d..f649fb76 100644 --- a/readme.md +++ b/readme.md @@ -39,7 +39,7 @@ Audiobookshelf is a self-hosted audiobook and podcast server. Is there a feature you are looking for? [Suggest it](https://github.com/advplyr/audiobookshelf/issues/new/choose) -Join us on [Discord](https://discord.gg/pJsjuNCKRq) or [Matrix](https://matrix.to/#/#audiobookshelf:matrix.org) +Join us on [Discord](https://discord.gg/HQgCbd6E75) or [Matrix](https://matrix.to/#/#audiobookshelf:matrix.org) ### Android App (beta) Try it out on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app) From a0eb6bd3dc8fe9f6b0e1be8b531cc391fcdb17d8 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 5 Jan 2024 14:38:29 -0600 Subject: [PATCH 0294/2145] Fix:Refresh podcast episode table when new episodes are downloaded --- client/components/tables/podcast/LazyEpisodesTable.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/components/tables/podcast/LazyEpisodesTable.vue b/client/components/tables/podcast/LazyEpisodesTable.vue index b1fb03ac..f2c6f342 100644 --- a/client/components/tables/podcast/LazyEpisodesTable.vue +++ b/client/components/tables/podcast/LazyEpisodesTable.vue @@ -87,7 +87,7 @@ export default { watch: { libraryItem: { handler() { - this.init() + this.refresh() } } }, @@ -515,6 +515,10 @@ export default { filterSortChanged() { this.init() }, + refresh() { + this.episodesCopy = this.episodes.map((ep) => ({ ...ep })) + this.init() + }, init() { this.destroyEpisodeComponents() this.totalEpisodes = this.episodesList.length From eaf6bf29cc7063080aa91f9a200de4ff834fce88 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 5 Jan 2024 14:39:25 -0600 Subject: [PATCH 0295/2145] Fix:Improve performance for podcast rss feed episodes modal for large rss feeds --- .../components/modals/podcast/EpisodeFeed.vue | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/client/components/modals/podcast/EpisodeFeed.vue b/client/components/modals/podcast/EpisodeFeed.vue index 4a1b4753..b5d98a25 100644 --- a/client/components/modals/podcast/EpisodeFeed.vue +++ b/client/components/modals/podcast/EpisodeFeed.vue @@ -68,7 +68,9 @@ export default { selectAll: false, search: null, searchTimeout: null, - searchText: null + searchText: null, + downloadedEpisodeGuidMap: {}, + downloadedEpisodeUrlMap: {} } }, watch: { @@ -122,11 +124,13 @@ export default { }, 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 - }) + if (episode.guid && !!this.downloadedEpisodeGuidMap[episode.guid]) { + return true + } + if (this.downloadedEpisodeUrlMap[episode.cleanUrl]) { + return true + } + return false }, /** * UPDATE: As of v2.4.5 guid is used for matching existing downloaded episodes if it is found on the RSS feed. @@ -219,6 +223,14 @@ export default { }) }, init() { + this.downloadedEpisodeGuidMap = {} + this.downloadedEpisodeUrlMap = {} + + this.itemEpisodes.forEach((episode) => { + if (episode.guid) this.downloadedEpisodeGuidMap[episode.guid] = episode.id + if (episode.enclosure?.url) this.downloadedEpisodeUrlMap[this.getCleanEpisodeUrl(episode.enclosure.url)] = episode.id + }) + this.episodesCleaned = this.episodes .filter((ep) => ep.enclosure?.url) .map((_ep) => { From a426da534c89a33ff28e6299d690a82ab2fc2081 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 5 Jan 2024 14:45:25 -0600 Subject: [PATCH 0296/2145] Fix:Export OPML not escaping characters #2487 --- server/utils/generators/opmlGenerator.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server/utils/generators/opmlGenerator.js b/server/utils/generators/opmlGenerator.js index 8cc3f7fb..8fb7c87c 100644 --- a/server/utils/generators/opmlGenerator.js +++ b/server/utils/generators/opmlGenerator.js @@ -1,4 +1,5 @@ const xml = require('../../libs/xml') +const escapeForXML = require('../../libs/xml/escapeForXML') /** * Generate OPML file string for podcasts in a library @@ -12,18 +13,18 @@ module.exports.generate = (podcasts, indent = true) => { if (!podcast.feedURL) return const feedAttributes = { type: 'rss', - text: podcast.title, - title: podcast.title, - xmlUrl: podcast.feedURL + text: escapeForXML(podcast.title), + title: escapeForXML(podcast.title), + xmlUrl: escapeForXML(podcast.feedURL) } if (podcast.description) { - feedAttributes.description = podcast.description + feedAttributes.description = escapeForXML(podcast.description) } if (podcast.itunesPageUrl) { - feedAttributes.htmlUrl = podcast.itunesPageUrl + feedAttributes.htmlUrl = escapeForXML(podcast.itunesPageUrl) } if (podcast.language) { - feedAttributes.language = podcast.language + feedAttributes.language = escapeForXML(podcast.language) } bodyItems.push({ outline: { From 935e545caa718a39b7b66d19471e956c35d9d7c1 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 6 Jan 2024 14:13:39 -0600 Subject: [PATCH 0297/2145] Update readme for iOS beta full --- readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index f649fb76..a3b84f00 100644 --- a/readme.md +++ b/readme.md @@ -45,7 +45,9 @@ Join us on [Discord](https://discord.gg/HQgCbd6E75) or [Matrix](https://matrix.t Try it out on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app) ### iOS App (beta) -Available using Test Flight: https://testflight.apple.com/join/wiic7QIW - [Join the discussion](https://github.com/advplyr/audiobookshelf-app/discussions/60) +**Beta is currently full. Apple has a hard limit of 10k beta testers. Updates will be posted in Discord/Matrix.** + +Using Test Flight: https://testflight.apple.com/join/wiic7QIW ***(beta is full)*** ### Build your own tools & clients Check out the [API documentation](https://api.audiobookshelf.org/) From e88c1fa32979bbfa45f98a8135394674dd2bc2fd Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 6 Jan 2024 15:54:48 -0600 Subject: [PATCH 0298/2145] Update:Show tooltip for library item card titles that are truncated #2451 - Refactored tooltip so that they dont overflow the window --- client/components/cards/LazyBookCard.vue | 13 +++- client/components/ui/Tooltip.vue | 77 ++++++++++++++++++------ 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index c4d1345d..04b3ce59 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -8,10 +8,10 @@ <!-- Alternative bookshelf title/author/sort --> <div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }"> <div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }"> - <div class="flex items-center"> - <span class="truncate">{{ displayTitle }}</span> + <ui-tooltip :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center"> + <p ref="displayTitle" class="truncate">{{ displayTitle }}</p> <widgets-explicit-indicator :explicit="isExplicit" /> - </div> + </ui-tooltip> </div> <p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || ' ' }}</p> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> @@ -164,6 +164,7 @@ export default { imageReady: false, selected: false, isSelectionMode: false, + displayTitleTruncated: false, showCoverBg: false } }, @@ -642,6 +643,12 @@ export default { } this.libraryItem = libraryItem + + this.$nextTick(() => { + if (this.$refs.displayTitle) { + this.displayTitleTruncated = this.$refs.displayTitle.scrollWidth > this.$refs.displayTitle.clientWidth + } + }) }, clickCard(e) { if (this.processing) return diff --git a/client/components/ui/Tooltip.vue b/client/components/ui/Tooltip.vue index c1eabfc6..77245537 100644 --- a/client/components/ui/Tooltip.vue +++ b/client/components/ui/Tooltip.vue @@ -15,6 +15,13 @@ export default { type: String, default: 'right' }, + /** + * Delay showing the tooltip after X milliseconds of hovering + */ + delayOnShow: { + type: Number, + default: 0 + }, disabled: Boolean }, data() { @@ -22,7 +29,8 @@ export default { tooltip: null, tooltipId: null, isShowing: false, - hideTimeout: null + hideTimeout: null, + delayOnShowTimeout: null } }, watch: { @@ -59,29 +67,44 @@ export default { this.tooltip = tooltip }, setTooltipPosition(tooltip) { - var boxChow = this.$refs.box.getBoundingClientRect() + const boxRect = this.$refs.box.getBoundingClientRect() + + const shouldMount = !tooltip.isConnected - var shouldMount = !tooltip.isConnected // Calculate size of tooltip if (shouldMount) document.body.appendChild(tooltip) - var { width, height } = tooltip.getBoundingClientRect() + const tooltipRect = tooltip.getBoundingClientRect() if (shouldMount) tooltip.remove() - var top = 0 - var left = 0 + // Subtracting scrollbar size + const windowHeight = window.innerHeight - 8 + const windowWidth = window.innerWidth - 8 + + let top = 0 + let left = 0 if (this.direction === 'right') { - top = boxChow.top - height / 2 + boxChow.height / 2 - left = boxChow.left + boxChow.width + 4 + top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2) + left = Math.max(0, boxRect.left + boxRect.width + 4) } else if (this.direction === 'bottom') { - top = boxChow.top + boxChow.height + 4 - left = boxChow.left - width / 2 + boxChow.width / 2 + top = Math.max(0, boxRect.top + boxRect.height + 4) + left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2) } else if (this.direction === 'top') { - top = boxChow.top - height - 4 - left = boxChow.left - width / 2 + boxChow.width / 2 + top = Math.max(0, boxRect.top - tooltipRect.height - 4) + left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2) } else if (this.direction === 'left') { - top = boxChow.top - height / 2 + boxChow.height / 2 - left = boxChow.left - width - 4 + top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2) + left = Math.max(0, boxRect.left - tooltipRect.width - 4) } + + // Shift left if tooltip would overflow the window on the right + if (left + tooltipRect.width > windowWidth) { + left -= left + tooltipRect.width - windowWidth + } + // Shift up if tooltip would overflow the window on the bottom + if (top + tooltipRect.height > windowHeight) { + top -= top + tooltipRect.height - windowHeight + } + tooltip.style.top = top + 'px' tooltip.style.left = left + 'px' }, @@ -107,15 +130,33 @@ export default { this.isShowing = false }, cancelHide() { - if (this.hideTimeout) clearTimeout(this.hideTimeout) + clearTimeout(this.hideTimeout) }, mouseover() { - if (!this.isShowing) this.showTooltip() + if (this.isShowing || this.disabled) return + + if (this.delayOnShow) { + if (this.delayOnShowTimeout) { + // Delay already running + return + } + + this.delayOnShowTimeout = setTimeout(() => { + this.showTooltip() + this.delayOnShowTimeout = null + }, this.delayOnShow) + } else { + this.showTooltip() + } }, mouseleave() { - if (this.isShowing) { - this.hideTimeout = setTimeout(this.hideTooltip, 100) + if (!this.isShowing) { + clearTimeout(this.delayOnShowTimeout) + this.delayOnShowTimeout = null + return } + + this.hideTimeout = setTimeout(this.hideTooltip, 100) } }, beforeDestroy() { From 4608f91ec610f7d639b4e5a3c81e7bcc2befaf29 Mon Sep 17 00:00:00 2001 From: Machou <Machou@users.noreply.github.com> Date: Sun, 7 Jan 2024 02:41:16 +0100 Subject: [PATCH 0299/2145] Update fr.json --- client/strings/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index e570614d..d894412c 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -154,7 +154,7 @@ "HeaderSchedule": "Programmation", "HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque", "HeaderSession": "Session", - "HeaderSetBackupSchedule": "Activer la Sauvegarde Automatique", + "HeaderSetBackupSchedule": "Activer la sauvegarde automatique", "HeaderSettings": "Paramètres", "HeaderSettingsDisplay": "Affichage", "HeaderSettingsExperimental": "Fonctionnalités expérimentales", From 69e23ef9f2b4f1d23549e7bcf2eafc8b9c447c2c Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 7 Jan 2024 17:51:07 -0600 Subject: [PATCH 0300/2145] Add:Epub metadata parser and cover extractor #1479 --- .../libraries/LibraryScannerSettings.vue | 2 +- server/managers/CoverManager.js | 41 +++++++ server/scanner/AbsMetadataFileScanner.js | 2 + server/scanner/BookScanner.js | 107 ++++++++++++----- server/scanner/PodcastScanner.js | 1 - server/utils/parsers/parseEbookMetadata.js | 42 +++++++ server/utils/parsers/parseEpubMetadata.js | 109 ++++++++++++++++++ server/utils/parsers/parseOpfMetadata.js | 15 +-- 8 files changed, 284 insertions(+), 35 deletions(-) create mode 100644 server/utils/parsers/parseEbookMetadata.js create mode 100644 server/utils/parsers/parseEpubMetadata.js diff --git a/client/components/modals/libraries/LibraryScannerSettings.vue b/client/components/modals/libraries/LibraryScannerSettings.vue index 8ec73dd0..43938f9c 100644 --- a/client/components/modals/libraries/LibraryScannerSettings.vue +++ b/client/components/modals/libraries/LibraryScannerSettings.vue @@ -63,7 +63,7 @@ export default { }, audioMetatags: { id: 'audioMetatags', - name: 'Audio file meta tags', + name: 'Audio file meta tags OR ebook metadata', include: true }, nfoFile: { diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js index 3cf97f33..9b4aa32d 100644 --- a/server/managers/CoverManager.js +++ b/server/managers/CoverManager.js @@ -7,6 +7,8 @@ const imageType = require('../libs/imageType') const globals = require('../utils/globals') const { downloadImageFile, filePathToPOSIX, checkPathIsFile } = require('../utils/fileUtils') const { extractCoverArt } = require('../utils/ffmpegHelpers') +const parseEbookMetadata = require('../utils/parsers/parseEbookMetadata') + const CacheManager = require('../managers/CacheManager') class CoverManager { @@ -234,6 +236,7 @@ class CoverManager { /** * Extract cover art from audio file and save for library item + * * @param {import('../models/Book').AudioFileObject[]} audioFiles * @param {string} libraryItemId * @param {string} [libraryItemPath] null for isFile library items @@ -268,6 +271,44 @@ class CoverManager { return null } + /** + * Extract cover art from ebook and save for library item + * + * @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData + * @param {string} libraryItemId + * @param {string} [libraryItemPath] null for isFile library items + * @returns {Promise<string>} returns cover path + */ + async saveEbookCoverArt(ebookFileScanData, libraryItemId, libraryItemPath) { + if (!ebookFileScanData?.ebookCoverPath) return null + + let coverDirPath = null + if (global.ServerSettings.storeCoverWithItem && libraryItemPath) { + coverDirPath = libraryItemPath + } else { + coverDirPath = Path.posix.join(global.MetadataPath, 'items', libraryItemId) + } + await fs.ensureDir(coverDirPath) + + let extname = Path.extname(ebookFileScanData.ebookCoverPath) || '.jpg' + if (extname === '.jpeg') extname = '.jpg' + const coverFilename = `cover${extname}` + const coverFilePath = Path.join(coverDirPath, coverFilename) + + // TODO: Overwrite if exists? + const coverAlreadyExists = await fs.pathExists(coverFilePath) + if (coverAlreadyExists) { + Logger.warn(`[CoverManager] Extract embedded cover art but cover already exists for "${coverFilePath}" - overwriting`) + } + + const success = await parseEbookMetadata.extractCoverImage(ebookFileScanData, coverFilePath) + if (success) { + await CacheManager.purgeCoverCache(libraryItemId) + return coverFilePath + } + return null + } + /** * * @param {string} url diff --git a/server/scanner/AbsMetadataFileScanner.js b/server/scanner/AbsMetadataFileScanner.js index 1f9d2823..e554dfb4 100644 --- a/server/scanner/AbsMetadataFileScanner.js +++ b/server/scanner/AbsMetadataFileScanner.js @@ -36,6 +36,8 @@ class AbsMetadataFileScanner { for (const key in abMetadata) { // TODO: When to override with null or empty arrays? if (abMetadata[key] === undefined || abMetadata[key] === null) continue + if (key === 'authors' && !abMetadata.authors?.length) continue + if (key === 'genres' && !abMetadata.genres?.length) continue if (key === 'tags' && !abMetadata.tags?.length) continue if (key === 'chapters' && !abMetadata.chapters?.length) continue diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 6c93dddf..b40e9323 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -3,8 +3,8 @@ const Path = require('path') const sequelize = require('sequelize') const { LogLevel } = require('../utils/constants') const { getTitleIgnorePrefix, areEquivalent } = require('../utils/index') -const abmetadataGenerator = require('../utils/generators/abmetadataGenerator') const parseNameString = require('../utils/parsers/parseNameString') +const parseEbookMetadata = require('../utils/parsers/parseEbookMetadata') const globals = require('../utils/globals') const AudioFileScanner = require('./AudioFileScanner') const Database = require('../Database') @@ -170,7 +170,9 @@ class BookScanner { hasMediaChanges = true } - const bookMetadata = await this.getBookMetadataFromScanData(media.audioFiles, libraryItemData, libraryScan, librarySettings, existingLibraryItem.id) + const ebookFileScanData = await parseEbookMetadata.parse(media.ebookFile) + + const bookMetadata = await this.getBookMetadataFromScanData(media.audioFiles, ebookFileScanData, libraryItemData, libraryScan, librarySettings, existingLibraryItem.id) let authorsUpdated = false const bookAuthorsRemoved = [] let seriesUpdated = false @@ -317,24 +319,34 @@ class BookScanner { }) } - // If no cover then extract cover from audio file if available OR search for cover if enabled in server settings + // If no cover then extract cover from audio file OR from ebook + const libraryItemDir = existingLibraryItem.isFile ? null : existingLibraryItem.path if (!media.coverPath) { - const libraryItemDir = existingLibraryItem.isFile ? null : existingLibraryItem.path - const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(media.audioFiles, existingLibraryItem.id, libraryItemDir) + let extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(media.audioFiles, existingLibraryItem.id, libraryItemDir) if (extractedCoverPath) { libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`) media.coverPath = extractedCoverPath hasMediaChanges = true - } else if (Database.serverSettings.scannerFindCovers) { - const authorName = media.authors.map(au => au.name).filter(au => au).join(', ') - const coverPath = await this.searchForCover(existingLibraryItem.id, libraryItemDir, media.title, authorName, libraryScan) - if (coverPath) { - media.coverPath = coverPath + } else if (ebookFileScanData?.ebookCoverPath) { + extractedCoverPath = await CoverManager.saveEbookCoverArt(ebookFileScanData, existingLibraryItem.id, libraryItemDir) + if (extractedCoverPath) { + libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" extracted embedded cover art from ebook file to path "${extractedCoverPath}"`) + media.coverPath = extractedCoverPath hasMediaChanges = true } } } + // If no cover then search for cover if enabled in server settings + if (!media.coverPath && Database.serverSettings.scannerFindCovers) { + const authorName = media.authors.map(au => au.name).filter(au => au).join(', ') + const coverPath = await this.searchForCover(existingLibraryItem.id, libraryItemDir, media.title, authorName, libraryScan) + if (coverPath) { + media.coverPath = coverPath + hasMediaChanges = true + } + } + existingLibraryItem.media = media let libraryItemUpdated = false @@ -408,12 +420,14 @@ class BookScanner { return null } + let ebookFileScanData = null if (ebookLibraryFile) { ebookLibraryFile = ebookLibraryFile.toJSON() ebookLibraryFile.ebookFormat = ebookLibraryFile.metadata.ext.slice(1).toLowerCase() + ebookFileScanData = await parseEbookMetadata.parse(ebookLibraryFile) } - const bookMetadata = await this.getBookMetadataFromScanData(scannedAudioFiles, libraryItemData, libraryScan, librarySettings) + const bookMetadata = await this.getBookMetadataFromScanData(scannedAudioFiles, ebookFileScanData, libraryItemData, libraryScan, librarySettings) bookMetadata.explicit = !!bookMetadata.explicit // Ensure boolean bookMetadata.abridged = !!bookMetadata.abridged // Ensure boolean @@ -481,19 +495,28 @@ class BookScanner { } } - // If cover was not found in folder then check embedded covers in audio files OR search for cover + // If cover was not found in folder then check embedded covers in audio files OR ebook file + const libraryItemDir = libraryItemObj.isFile ? null : libraryItemObj.path if (!bookObject.coverPath) { - const libraryItemDir = libraryItemObj.isFile ? null : libraryItemObj.path - // Extract and save embedded cover art - const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(scannedAudioFiles, libraryItemObj.id, libraryItemDir) + let extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(scannedAudioFiles, libraryItemObj.id, libraryItemDir) if (extractedCoverPath) { + libraryScan.addLog(LogLevel.DEBUG, `Extracted embedded cover from audio file at "${extractedCoverPath}" for book "${bookObject.title}"`) bookObject.coverPath = extractedCoverPath - } else if (Database.serverSettings.scannerFindCovers) { - const authorName = bookMetadata.authors.join(', ') - bookObject.coverPath = await this.searchForCover(libraryItemObj.id, libraryItemDir, bookObject.title, authorName, libraryScan) + } else if (ebookFileScanData?.ebookCoverPath) { + extractedCoverPath = await CoverManager.saveEbookCoverArt(ebookFileScanData, libraryItemObj.id, libraryItemDir) + if (extractedCoverPath) { + libraryScan.addLog(LogLevel.DEBUG, `Extracted embedded cover from ebook file at "${extractedCoverPath}" for book "${bookObject.title}"`) + bookObject.coverPath = extractedCoverPath + } } } + // If cover not found then search for cover if enabled in settings + if (!bookObject.coverPath && Database.serverSettings.scannerFindCovers) { + const authorName = bookMetadata.authors.join(', ') + bookObject.coverPath = await this.searchForCover(libraryItemObj.id, libraryItemDir, bookObject.title, authorName, libraryScan) + } + libraryItemObj.book = bookObject const libraryItem = await Database.libraryItemModel.create(libraryItemObj, { include: { @@ -570,13 +593,14 @@ class BookScanner { /** * * @param {import('../models/Book').AudioFileObject[]} audioFiles + * @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData * @param {import('./LibraryItemScanData')} libraryItemData * @param {LibraryScan} libraryScan * @param {import('../models/Library').LibrarySettingsObject} librarySettings * @param {string} [existingLibraryItemId] * @returns {Promise<BookMetadataObject>} */ - async getBookMetadataFromScanData(audioFiles, libraryItemData, libraryScan, librarySettings, existingLibraryItemId = null) { + async getBookMetadataFromScanData(audioFiles, ebookFileScanData, libraryItemData, libraryScan, librarySettings, existingLibraryItemId = null) { // First set book metadata from folder/file names const bookMetadata = { title: libraryItemData.mediaMetadata.title, // required @@ -599,7 +623,7 @@ class BookScanner { coverPath: undefined } - const bookMetadataSourceHandler = new BookScanner.BookMetadataSourceHandler(bookMetadata, audioFiles, libraryItemData, libraryScan, existingLibraryItemId) + const bookMetadataSourceHandler = new BookScanner.BookMetadataSourceHandler(bookMetadata, audioFiles, ebookFileScanData, libraryItemData, libraryScan, existingLibraryItemId) 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) { @@ -627,13 +651,15 @@ class BookScanner { * * @param {Object} bookMetadata * @param {import('../models/Book').AudioFileObject[]} audioFiles + * @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData * @param {import('./LibraryItemScanData')} libraryItemData * @param {LibraryScan} libraryScan * @param {string} existingLibraryItemId */ - constructor(bookMetadata, audioFiles, libraryItemData, libraryScan, existingLibraryItemId) { + constructor(bookMetadata, audioFiles, ebookFileScanData, libraryItemData, libraryScan, existingLibraryItemId) { this.bookMetadata = bookMetadata this.audioFiles = audioFiles + this.ebookFileScanData = ebookFileScanData this.libraryItemData = libraryItemData this.libraryScan = libraryScan this.existingLibraryItemId = existingLibraryItemId @@ -647,13 +673,42 @@ class BookScanner { } /** - * Metadata from audio file meta tags + * Metadata from audio file meta tags OR metadata from ebook file */ audioMetatags() { - if (!this.audioFiles.length) return - // Modifies bookMetadata with metadata mapped from audio file meta tags - const bookTitle = this.bookMetadata.title || this.libraryItemData.mediaMetadata.title - AudioFileScanner.setBookMetadataFromAudioMetaTags(bookTitle, this.audioFiles, this.bookMetadata, this.libraryScan) + if (this.audioFiles.length) { + // Modifies bookMetadata with metadata mapped from audio file meta tags + const bookTitle = this.bookMetadata.title || this.libraryItemData.mediaMetadata.title + AudioFileScanner.setBookMetadataFromAudioMetaTags(bookTitle, this.audioFiles, this.bookMetadata, this.libraryScan) + } else if (this.ebookFileScanData) { + const ebookMetdataObject = this.ebookFileScanData.metadata + for (const key in ebookMetdataObject) { + if (key === 'tags') { + if (ebookMetdataObject.tags.length) { + this.bookMetadata.tags = ebookMetdataObject.tags + } + } else if (key === 'genres') { + if (ebookMetdataObject.genres.length) { + this.bookMetadata.genres = ebookMetdataObject.genres + } + } else if (key === 'authors') { + if (ebookMetdataObject.authors?.length) { + this.bookMetadata.authors = ebookMetdataObject.authors + } + } else if (key === 'narrators') { + if (ebookMetdataObject.narrators?.length) { + this.bookMetadata.narrators = ebookMetdataObject.narrators + } + } else if (key === 'series') { + if (ebookMetdataObject.series?.length) { + this.bookMetadata.series = ebookMetdataObject.series + } + } else if (ebookMetdataObject[key] && key !== 'sequence') { + this.bookMetadata[key] = ebookMetdataObject[key] + } + } + } + return null } /** diff --git a/server/scanner/PodcastScanner.js b/server/scanner/PodcastScanner.js index b56c4db6..07dcbb11 100644 --- a/server/scanner/PodcastScanner.js +++ b/server/scanner/PodcastScanner.js @@ -2,7 +2,6 @@ const uuidv4 = require("uuid").v4 const Path = require('path') const { LogLevel } = require('../utils/constants') const { getTitleIgnorePrefix } = require('../utils/index') -const abmetadataGenerator = require('../utils/generators/abmetadataGenerator') const AudioFileScanner = require('./AudioFileScanner') const Database = require('../Database') const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') diff --git a/server/utils/parsers/parseEbookMetadata.js b/server/utils/parsers/parseEbookMetadata.js new file mode 100644 index 00000000..6e97c1da --- /dev/null +++ b/server/utils/parsers/parseEbookMetadata.js @@ -0,0 +1,42 @@ +const parseEpubMetadata = require('./parseEpubMetadata') + +/** + * @typedef EBookFileScanData + * @property {string} path + * @property {string} ebookFormat + * @property {string} ebookCoverPath internal image path + * @property {import('../../scanner/BookScanner').BookMetadataObject} metadata + */ + +/** + * Parse metadata from ebook file + * + * @param {import('../../models/Book').EBookFileObject} ebookFile + * @returns {Promise<EBookFileScanData>} + */ +async function parse(ebookFile) { + if (!ebookFile) return null + + if (ebookFile.ebookFormat === 'epub') { + return parseEpubMetadata.parse(ebookFile.metadata.path) + } + return null +} +module.exports.parse = parse + +/** + * Extract cover from ebook file + * + * @param {EBookFileScanData} ebookFileScanData + * @param {string} outputCoverPath + * @returns {Promise<boolean>} + */ +async function extractCoverImage(ebookFileScanData, outputCoverPath) { + if (!ebookFileScanData?.ebookCoverPath) return false + + if (ebookFileScanData.ebookFormat === 'epub') { + return parseEpubMetadata.extractCoverImage(ebookFileScanData.path, ebookFileScanData.ebookCoverPath, outputCoverPath) + } + return false +} +module.exports.extractCoverImage = extractCoverImage \ No newline at end of file diff --git a/server/utils/parsers/parseEpubMetadata.js b/server/utils/parsers/parseEpubMetadata.js new file mode 100644 index 00000000..7238b0bf --- /dev/null +++ b/server/utils/parsers/parseEpubMetadata.js @@ -0,0 +1,109 @@ +const Path = require('path') +const Logger = require('../../Logger') +const StreamZip = require('../../libs/nodeStreamZip') +const parseOpfMetadata = require('./parseOpfMetadata') +const { xmlToJSON } = require('../index') + + +/** + * Extract file from epub and return string content + * + * @param {string} epubPath + * @param {string} filepath + * @returns {Promise<string>} + */ +async function extractFileFromEpub(epubPath, filepath) { + const zip = new StreamZip.async({ file: epubPath }) + const data = await zip.entryData(filepath).catch((error) => { + Logger.error(`[parseEpubMetadata] Failed to extract ${filepath} from epub at "${epubPath}"`, error) + }) + const filedata = data?.toString('utf8') + await zip.close() + return filedata +} + +/** + * Extract an XML file from epub and return JSON + * + * @param {string} epubPath + * @param {string} xmlFilepath + * @returns {Promise<Object>} + */ +async function extractXmlToJson(epubPath, xmlFilepath) { + const filedata = await extractFileFromEpub(epubPath, xmlFilepath) + if (!filedata) return null + return xmlToJSON(filedata) +} + +/** + * Extract cover image from epub return true if success + * + * @param {string} epubPath + * @param {string} epubImageFilepath + * @param {string} outputCoverPath + * @returns {Promise<boolean>} + */ +async function extractCoverImage(epubPath, epubImageFilepath, outputCoverPath) { + const zip = new StreamZip.async({ file: epubPath }) + + const success = await zip.extract(epubImageFilepath, outputCoverPath).then(() => true).catch((error) => { + Logger.error(`[parseEpubMetadata] Failed to extract image ${epubImageFilepath} from epub at "${epubPath}"`, error) + return false + }) + + await zip.close() + + return success +} +module.exports.extractCoverImage = extractCoverImage + +/** + * Parse metadata from epub + * + * @param {string} epubPath + * @returns {Promise<import('./parseEbookMetadata').EBookFileScanData>} + */ +async function parse(epubPath) { + Logger.debug(`Parsing metadata from epub at "${epubPath}"`) + // Entrypoint of the epub that contains the filepath to the package document (opf file) + const containerJson = await extractXmlToJson(epubPath, 'META-INF/container.xml') + + // Get package document opf filepath from container.xml + const packageDocPath = containerJson.container?.rootfiles?.[0]?.rootfile?.[0]?.$?.['full-path'] + if (!packageDocPath) { + Logger.error(`Failed to get package doc path in Container.xml`, JSON.stringify(containerJson, null, 2)) + return null + } + + // Extract package document to JSON + const packageJson = await extractXmlToJson(epubPath, packageDocPath) + if (!packageJson) { + return null + } + + // Parse metadata from package document opf file + const opfMetadata = parseOpfMetadata.parseOpfMetadataJson(packageJson) + if (!opfMetadata) { + Logger.error(`Unable to parse metadata in package doc with json`, JSON.stringify(packageJson, null, 2)) + return null + } + + const payload = { + path: epubPath, + ebookFormat: 'epub', + metadata: opfMetadata + } + + // Attempt to find filepath to cover image + const manifestFirstImage = packageJson.package?.manifest?.[0]?.item?.find(item => item.$?.['media-type']?.startsWith('image/')) + let coverImagePath = manifestFirstImage?.$?.href + if (coverImagePath) { + const packageDirname = Path.dirname(packageDocPath) + payload.ebookCoverPath = Path.posix.join(packageDirname, coverImagePath) + } else { + Logger.warn(`Cover image not found in manifest for epub at "${epubPath}"`) + } + + return payload +} +module.exports.parse = parse \ No newline at end of file diff --git a/server/utils/parsers/parseOpfMetadata.js b/server/utils/parsers/parseOpfMetadata.js index b51ceea5..3087497a 100644 --- a/server/utils/parsers/parseOpfMetadata.js +++ b/server/utils/parsers/parseOpfMetadata.js @@ -136,11 +136,7 @@ function stripPrefix(str) { return str.split(':').pop() } -module.exports.parseOpfMetadataXML = async (xml) => { - const json = await xmlToJSON(xml) - - if (!json) return null - +module.exports.parseOpfMetadataJson = (json) => { // Handle <package ...> or with prefix <ns0:package ...> const packageKey = Object.keys(json).find(key => stripPrefix(key) === 'package') if (!packageKey) return null @@ -167,7 +163,7 @@ module.exports.parseOpfMetadataXML = async (xml) => { const creators = parseCreators(metadata) const authors = (fetchCreators(creators, 'aut') || []).map(au => au?.trim()).filter(au => au) const narrators = (fetchNarrators(creators, metadata) || []).map(nrt => nrt?.trim()).filter(nrt => nrt) - const data = { + return { title: fetchTitle(metadata), subtitle: fetchSubtitle(metadata), authors, @@ -182,5 +178,10 @@ module.exports.parseOpfMetadataXML = async (xml) => { series: fetchSeries(metadataMeta), tags: fetchTags(metadata) } - return data +} + +module.exports.parseOpfMetadataXML = async (xml) => { + const json = await xmlToJSON(xml) + if (!json) return null + return this.parseOpfMetadataJson(json) } \ No newline at end of file From da25eff5c12d163e1329b3470ea60f79127c8e38 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 8 Jan 2024 18:21:15 -0600 Subject: [PATCH 0301/2145] Fix:Parse series sequence from OPF in cases where series_index is not directly underneath series meta #2505 --- server/utils/parsers/parseOpfMetadata.js | 13 +++++++++++-- .../utils/parsers/parseOpfMetadata.test.js | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/server/utils/parsers/parseOpfMetadata.js b/server/utils/parsers/parseOpfMetadata.js index 3087497a..a5419601 100644 --- a/server/utils/parsers/parseOpfMetadata.js +++ b/server/utils/parsers/parseOpfMetadata.js @@ -103,15 +103,24 @@ function fetchSeries(metadataMeta) { if (!metadataMeta) return [] const result = [] for (let i = 0; i < metadataMeta.length; i++) { - if (metadataMeta[i].$?.name === "calibre:series" && metadataMeta[i].$.content?.trim()) { + if (metadataMeta[i].$?.name === 'calibre:series' && metadataMeta[i].$.content?.trim()) { const name = metadataMeta[i].$.content.trim() let sequence = null - if (metadataMeta[i + 1]?.$?.name === "calibre:series_index" && metadataMeta[i + 1].$?.content?.trim()) { + if (metadataMeta[i + 1]?.$?.name === 'calibre:series_index' && metadataMeta[i + 1].$?.content?.trim()) { sequence = metadataMeta[i + 1].$.content.trim() } result.push({ name, sequence }) } } + + // If one series was found with no series_index then check if any series_index meta can be found + // this is to support when calibre:series_index is not directly underneath calibre:series + if (result.length === 1 && !result[0].sequence) { + const seriesIndexMeta = metadataMeta.find(m => m.$?.name === 'calibre:series_index' && m.$.content?.trim()) + if (seriesIndexMeta) { + result[0].sequence = seriesIndexMeta.$.content.trim() + } + } return result } diff --git a/test/server/utils/parsers/parseOpfMetadata.test.js b/test/server/utils/parsers/parseOpfMetadata.test.js index f1d5ce89..ca033cca 100644 --- a/test/server/utils/parsers/parseOpfMetadata.test.js +++ b/test/server/utils/parsers/parseOpfMetadata.test.js @@ -110,4 +110,21 @@ describe('parseOpfMetadata - test series', async () => { { "name": "Serie 1", "sequence": null } ]) }) + + it('test series and series index not directly underneath', async () => { + const opf = ` + <?xml version='1.0' encoding='UTF-8'?> + <package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid"> + <metadata> + <meta name="calibre:series" content="Serie 1"/> + <meta name="calibre:title_sort" content="Test Title"/> + <meta name="calibre:series_index" content="1"/> + </metadata> + </package> + ` + const parsedOpf = await parseOpfMetadataXML(opf) + expect(parsedOpf.series).to.deep.equal([ + { "name": "Serie 1", "sequence": "1" } + ]) + }) }) From 4a76059608651ce219dc9e18d41cb11f879008b4 Mon Sep 17 00:00:00 2001 From: Benjamin Porter <FreedomBen@users.noreply.github.com> Date: Wed, 3 Jan 2024 16:19:28 -0700 Subject: [PATCH 0302/2145] Change `Logger.dev` calls to `Logger.debug` Logger.dev is kind of in a weird spot where it doesn't fit into the standard log level. It is called directly by some code and it only checks whether a property is set (which comes from an env var) before deciding to print out. This standardizes on `debug` by changing the dev calls to debug. Also removes the now unused code. --- server/Database.js | 4 ++-- server/Logger.js | 10 ---------- server/models/Library.js | 2 +- server/models/LibraryItem.js | 36 ++++++++++++++++++------------------ 4 files changed, 21 insertions(+), 31 deletions(-) diff --git a/server/Database.js b/server/Database.js index fd606bac..0ddef620 100644 --- a/server/Database.js +++ b/server/Database.js @@ -177,11 +177,11 @@ class Database { if (process.env.QUERY_LOGGING === "log") { // Setting QUERY_LOGGING=log will log all Sequelize queries before they run Logger.info(`[Database] Query logging enabled`) - logging = (query) => Logger.dev(`Running the following query:\n ${query}`) + logging = (query) => Logger.debug(`Running the following query:\n ${query}`) } else if (process.env.QUERY_LOGGING === "benchmark") { // Setting QUERY_LOGGING=benchmark will log all Sequelize queries and their execution times, after they run Logger.info(`[Database] Query benchmarking enabled"`) - logging = (query, time) => Logger.dev(`Ran the following query in ${time}ms:\n ${query}`) + logging = (query, time) => Logger.debug(`Ran the following query in ${time}ms:\n ${query}`) benchmark = true } diff --git a/server/Logger.js b/server/Logger.js index b4953189..54fa5802 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -5,7 +5,6 @@ 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 @@ -88,15 +87,6 @@ class Logger { this.debug(`Set Log Level to ${this.levelString}`) } - /** - * Only to console and only for development - * @param {...any} args - */ - dev(...args) { - if (this.hideDevLogs) return - console.log(`[${this.timestamp}] DEV:`, ...args) - } - trace(...args) { if (this.logLevel > LogLevel.TRACE) return console.trace(`[${this.timestamp}] TRACE:`, ...args) diff --git a/server/models/Library.js b/server/models/Library.js index df202fb9..c6875ad7 100644 --- a/server/models/Library.js +++ b/server/models/Library.js @@ -233,7 +233,7 @@ class Library extends Model { for (let i = 0; i < libraries.length; i++) { const library = libraries[i] if (library.displayOrder !== i + 1) { - Logger.dev(`[Library] Updating display order of library from ${library.displayOrder} to ${i + 1}`) + Logger.debug(`[Library] Updating display order of library from ${library.displayOrder} to ${i + 1}`) await library.update({ displayOrder: i + 1 }).catch((error) => { Logger.error(`[Library] Failed to update library display order to ${i + 1}`, error) }) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 67e9abfb..508cf4c6 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -264,7 +264,7 @@ class LibraryItem extends Model { for (const existingPodcastEpisode of existingPodcastEpisodes) { // Episode was removed if (!updatedPodcastEpisodes.some(ep => ep.id === existingPodcastEpisode.id)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingPodcastEpisode.title}" was removed`) + Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingPodcastEpisode.title}" was removed`) await existingPodcastEpisode.destroy() hasUpdates = true } @@ -272,7 +272,7 @@ class LibraryItem extends Model { for (const updatedPodcastEpisode of updatedPodcastEpisodes) { const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id) if (!existingEpisodeMatch) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`) + Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`) await this.sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode) hasUpdates = true } else { @@ -283,7 +283,7 @@ class LibraryItem extends Model { if (existingValue instanceof Date) existingValue = existingValue.valueOf() if (!areEquivalent(updatedEpisodeCleaned[key], existingValue, true)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingEpisodeMatch.title}" ${key} was updated from "${existingValue}" to "${updatedEpisodeCleaned[key]}"`) + Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingEpisodeMatch.title}" ${key} was updated from "${existingValue}" to "${updatedEpisodeCleaned[key]}"`) episodeHasUpdates = true } } @@ -304,7 +304,7 @@ class LibraryItem extends Model { for (const existingAuthor of existingAuthors) { // Author was removed from Book if (!updatedAuthors.some(au => au.id === existingAuthor.id)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`) + Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`) await this.sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id) hasUpdates = true } @@ -312,7 +312,7 @@ class LibraryItem extends Model { for (const updatedAuthor of updatedAuthors) { // Author was added if (!existingAuthors.some(au => au.id === updatedAuthor.id)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`) + Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`) await this.sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id }) hasUpdates = true } @@ -320,7 +320,7 @@ class LibraryItem extends Model { for (const existingSeries of existingSeriesAll) { // Series was removed if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`) + Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`) await this.sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id) hasUpdates = true } @@ -329,11 +329,11 @@ class LibraryItem extends Model { // Series was added/updated const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id) if (!existingSeriesMatch) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`) + Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`) await this.sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence }) hasUpdates = true } else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`) + Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`) await existingSeriesMatch.bookSeries.update({ id: updatedSeries.id, sequence: updatedSeries.sequence }) hasUpdates = true } @@ -346,7 +346,7 @@ class LibraryItem extends Model { if (existingValue instanceof Date) existingValue = existingValue.valueOf() if (!areEquivalent(updatedMedia[key], existingValue, true)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from ${existingValue} to ${updatedMedia[key]}`) + Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from ${existingValue} to ${updatedMedia[key]}`) hasMediaUpdates = true } } @@ -363,7 +363,7 @@ class LibraryItem extends Model { if (existingValue instanceof Date) existingValue = existingValue.valueOf() if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) { - Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`) + Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`) hasLibraryItemUpdates = true } } @@ -541,7 +541,7 @@ class LibraryItem extends Model { }) } } - Logger.dev(`Loaded ${itemsInProgressPayload.items.length} of ${itemsInProgressPayload.count} items for "Continue Listening/Reading" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${itemsInProgressPayload.items.length} of ${itemsInProgressPayload.count} items for "Continue Listening/Reading" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) let start = Date.now() if (library.isBook) { @@ -558,7 +558,7 @@ class LibraryItem extends Model { total: continueSeriesPayload.count }) } - Logger.dev(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } else if (library.isPodcast) { // "Newest Episodes" shelf const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, user, limit) @@ -572,7 +572,7 @@ class LibraryItem extends Model { total: newestEpisodesPayload.count }) } - Logger.dev(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } start = Date.now() @@ -588,7 +588,7 @@ class LibraryItem extends Model { total: mostRecentPayload.count }) } - Logger.dev(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`) if (library.isBook) { start = Date.now() @@ -604,7 +604,7 @@ class LibraryItem extends Model { total: seriesMostRecentPayload.count }) } - Logger.dev(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) start = Date.now() // "Discover" shelf @@ -619,7 +619,7 @@ class LibraryItem extends Model { total: discoverLibraryItemsPayload.count }) } - Logger.dev(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } start = Date.now() @@ -650,7 +650,7 @@ class LibraryItem extends Model { }) } } - Logger.dev(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`) if (library.isBook) { start = Date.now() @@ -666,7 +666,7 @@ class LibraryItem extends Model { total: newestAuthorsPayload.count }) } - Logger.dev(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } Logger.debug(`Loaded ${shelves.length} personalized shelves in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) From e8fa029df77231d38d6cd23f327985d6a60a4461 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 10 Jan 2024 08:12:26 -0600 Subject: [PATCH 0303/2145] Fix:Specific podcast rss feed cannot be fetched due to accept header #2446 --- server/utils/podcastUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 4e01c92b..769798eb 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -233,7 +233,7 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { method: 'GET', timeout: 12000, responseType: 'arraybuffer', - headers: { Accept: 'application/rss+xml, application/xhtml+xml, application/xml' }, + headers: { Accept: 'application/rss+xml, application/xhtml+xml, application/xml, */*;q=0.8' }, httpAgent: ssrfFilter(feedUrl), httpsAgent: ssrfFilter(feedUrl) }).then(async (data) => { From cf85d66b2f59412fb01f2601685669138656278a Mon Sep 17 00:00:00 2001 From: Torstein Eide <1884894+Eideen@users.noreply.github.com> Date: Fri, 12 Jan 2024 14:26:32 +0100 Subject: [PATCH 0304/2145] Add example for HAproxy --- readme.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/readme.md b/readme.md index a3b84f00..4c8bd5d7 100644 --- a/readme.md +++ b/readme.md @@ -241,6 +241,93 @@ subdomain.domain.com { reverse_proxy <LOCAL_IP>:<PORT> } ``` +### HAproxy + +Bellow is a geneic HAproxy config, using `audiobookshelf.YOUR_DOMAIN.COM`. + +To use `http2`, `ssl` is needed. + +````make +global + # ... (your global settings go here) + +defaults + mode http + # ... (your default settings go here) + +frontend my_frontend + # Bind to port 443, enable SSL, and specify the certificate list file + bind :443 name :443 ssl crt-list /path/to/cert.crt_list alpn h2,http/1.1 + mode http + + # Define an ACL for subdomains starting with "audiobookshelf" + acl is_audiobookshelf hdr_beg(host) -i audiobookshelf + + # Use the ACL to route traffic to audiobookshelf_backend if the condition is met, + # otherwise, use the default_backend + use_backend audiobookshelf_backend if is_audiobookshelf + default_backend default_backend + +backend audiobookshelf_backend + mode http + # ... (backend settings for audiobookshelf go here) + + # Define the server for the audiobookshelf backend + server audiobookshelf_server 127.0.0.99:13378 + +backend default_backend + mode http + # ... (default backend settings go here) + + # Define the server for the default backend + server default_server 127.0.0.123:8081 + +```` + +#### PFsense and HAproxy + +For PFsense the inputs are more graficals + +##### Frontend, Default backend, access control lists and actions + +###### Access Control lists + +| Name | Expression | CS | Not | Value | +|:--------------:|:-----------------:|:--:|:---:|:---------------:| +| audiobookshelf | Host starts with: | | | audiobookshelf. | + + + +###### Actions + +The `condition acl names` needs to match the name above `audiobookshelf`. + +| Action | Parameters | Condition acl names | Backend | +|:--------------:|:-----------------:|:---------------:|:---------------:| +| audiobookshelf | Host starts with: | audiobookshelf. | audiobookshelf| + +##### Backend + + +The `Name` needs to match the `Backend` above `audiobookshelf`. + +| Name | audiobookshelf | +|--------------|-----------------| + +**Server list:** + +| Name | Expression | CS | Not | Value | +|:--------------:|:-----------------:|:--:|:---:|:---------------:| +| audiobookshelf | Host starts with: | | | audiobookshelf. | + +##### Health checking + +Health checking is enabled by default. `Http check method` of `OPTIONS` is not supported on Audiobookshelf. +If Health check fails, data will not be forwared. +Need to one of following: + +* Change `Health check method` to `none`. To disable. +* Change `Http check method` to `HEAD` or `GET`. To make Health checking function. # Run from source From 6ca684603c6c1a1b0737b62e1663913e7a74a555 Mon Sep 17 00:00:00 2001 From: Torstein Eide <1884894+Eideen@users.noreply.github.com> Date: Fri, 12 Jan 2024 14:35:30 +0100 Subject: [PATCH 0305/2145] Fix typos --- readme.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/readme.md b/readme.md index 4c8bd5d7..86fa46f0 100644 --- a/readme.md +++ b/readme.md @@ -284,13 +284,13 @@ backend default_backend ```` -#### PFsense and HAproxy +### PFsense and HAproxy -For PFsense the inputs are more graficals +For PFsense the inputs are graphical, and `Health checking` is enabled. -##### Frontend, Default backend, access control lists and actions +#### Frontend, Default backend, access control lists and actions -###### Access Control lists +##### Access Control lists | Name | Expression | CS | Not | Value | |:--------------:|:-----------------:|:--:|:---:|:---------------:| @@ -298,29 +298,29 @@ For PFsense the inputs are more graficals -###### Actions +##### Actions The `condition acl names` needs to match the name above `audiobookshelf`. -| Action | Parameters | Condition acl names | Backend | -|:--------------:|:-----------------:|:---------------:|:---------------:| -| audiobookshelf | Host starts with: | audiobookshelf. | audiobookshelf| +| Action | Parameters | Condition acl names | +|:--------------:|:-----------------:|:---------------:| +| `Use Backend` |audiobookshelf | audiobookshelf | -##### Backend +#### Backend -The `Name` needs to match the `Backend` above `audiobookshelf`. +The `Name` needs to match the `Parameters` above `audiobookshelf`. | Name | audiobookshelf | |--------------|-----------------| -**Server list:** +##### Server list: | Name | Expression | CS | Not | Value | |:--------------:|:-----------------:|:--:|:---:|:---------------:| | audiobookshelf | Host starts with: | | | audiobookshelf. | -##### Health checking +##### Health checking: Health checking is enabled by default. `Http check method` of `OPTIONS` is not supported on Audiobookshelf. If Health check fails, data will not be forwared. From 3b531144cfdbce2379b90bb4f2ee36ca949db79c Mon Sep 17 00:00:00 2001 From: FlyinPancake <FlyinPancake@users.noreply.github.com> Date: Fri, 12 Jan 2024 21:45:03 +0100 Subject: [PATCH 0306/2145] implemented suggestions, extended CMPs with series --- .../pages/config/custom-metadata-providers.vue | 6 ++---- client/strings/en-us.json | 6 +++--- custom-metadata-provider-specification.yaml | 13 ++++++++++++- server/controllers/MiscController.js | 10 +++++----- server/models/CustomMetadataProvider.js | 10 ++-------- server/providers/CustomProviderAdapter.js | 16 +++++++++------- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/client/pages/config/custom-metadata-providers.vue b/client/pages/config/custom-metadata-providers.vue index 10fdb21b..9f394eae 100644 --- a/client/pages/config/custom-metadata-providers.vue +++ b/client/pages/config/custom-metadata-providers.vue @@ -9,7 +9,7 @@ </ui-tooltip> <div class="flex-grow" /> - <ui-btn color="primary" small @click="setShowAddModal()">{{ $strings.ButtonAdd }}</ui-btn> + <ui-btn color="primary" small @click="setShowAddModal">{{ $strings.ButtonAdd }}</ui-btn> </template> <tables-custom-metadata-provider-table class="pt-2" /> @@ -40,6 +40,4 @@ export default { } </script> -<style> - -</style> +<style></style> diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 9dfde095..e937ed72 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -96,7 +96,6 @@ "HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudioTracks": "Audio Tracks", "HeaderAuthentication": "Authentication", - "HeaderCustomMetadataProviders": "Custom metadata providers", "HeaderBackups": "Backups", "HeaderChangePassword": "Change Password", "HeaderChapters": "Chapters", @@ -105,6 +104,7 @@ "HeaderCollectionItems": "Collection Items", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Current Downloads", + "HeaderCustomMetadataProviders": "Custom metadata providers", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Queue", "HeaderEbookFiles": "Ebook Files", @@ -194,6 +194,7 @@ "LabelAllUsersExcludingGuests": "All users excluding guests", "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Already in your library", + "LabelApiKey": "API Key", "LabelAppend": "Append", "LabelAuthor": "Author", "LabelAuthorFirstLast": "Author (First Last)", @@ -526,6 +527,7 @@ "LabelUploaderDragAndDrop": "Drag & drop files or folders", "LabelUploaderDropFiles": "Drop files", "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", + "LabelUrl": "URL", "LabelUseChapterTrack": "Use chapter track", "LabelUseFullTrack": "Use full track", "LabelUser": "User", @@ -541,8 +543,6 @@ "LabelYourBookmarks": "Your Bookmarks", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Your Progress", - "LabelUrl": "URL", - "LabelApiKey": "API Key", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.", diff --git a/custom-metadata-provider-specification.yaml b/custom-metadata-provider-specification.yaml index 3201fbb8..90df875b 100644 --- a/custom-metadata-provider-specification.yaml +++ b/custom-metadata-provider-specification.yaml @@ -86,7 +86,7 @@ components: type: string publisher: type: string - published_year: + publishedYear: type: string description: type: string @@ -107,6 +107,17 @@ components: type: array items: type: string + series: + type: array + items: + type: object + properties: + series: + type: string + required: true + sequence: + type: number + format: int64 language: type: string duration: diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 76140dcc..1d2fff04 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -763,7 +763,7 @@ class MiscController { return res.sendStatus(403) } - const { name, url, apiKey } = req.body; + const { name, url, apiKey } = req.body if (!name || !url || !apiKey) { return res.status(500).send(`Invalid patch data`) @@ -794,18 +794,18 @@ class MiscController { return res.sendStatus(403) } - const { id } = req.params; + const { id } = req.params if (!id) { return res.status(500).send(`Invalid delete data`) } - const provider = await Database.customMetadataProviderModel.findByPk(id); - await Database.removeCustomMetadataProviderById(id); + const provider = await Database.customMetadataProviderModel.findByPk(id) + await Database.removeCustomMetadataProviderById(id) SocketAuthority.adminEmitter('custom_metadata_provider_removed', provider) - res.json({}) + res.sendStatus(200) } } module.exports = new MiscController() diff --git a/server/models/CustomMetadataProvider.js b/server/models/CustomMetadataProvider.js index 9bc175c4..d6047bb8 100644 --- a/server/models/CustomMetadataProvider.js +++ b/server/models/CustomMetadataProvider.js @@ -26,13 +26,7 @@ class CustomMetadataProvider extends Model { } } - static findByPk(id) { - return this.findOne({ - where: { - id, - } - }) - } + /** * Initialize model @@ -47,7 +41,7 @@ class CustomMetadataProvider extends Model { }, name: DataTypes.STRING, url: DataTypes.STRING, - apiKey: DataTypes.STRING + apiKey: DataTypes.STRING, }, { sequelize, modelName: 'customMetadataProvider' diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js index d5f64291..1919ecc9 100644 --- a/server/providers/CustomProviderAdapter.js +++ b/server/providers/CustomProviderAdapter.js @@ -1,6 +1,6 @@ const Database = require('../Database') -const axios = require("axios"); -const Logger = require("../Logger"); +const axios = require("axios") +const Logger = require("../Logger") class CustomProviderAdapter { constructor() { @@ -8,10 +8,10 @@ class CustomProviderAdapter { async search(title, author, providerSlug) { const providerId = providerSlug.split("custom-")[1] - const provider = await Database.customMetadataProviderModel.findByPk(providerId); + const provider = await Database.customMetadataProviderModel.findByPk(providerId) if (!provider) { - throw new Error("Custom provider not found for the given id"); + throw new Error("Custom provider not found for the given id") } const matches = await axios.get(`${provider.url}/search?query=${encodeURIComponent(title)}${!!author ? `&author=${encodeURIComponent(author)}` : ""}`, { @@ -27,7 +27,7 @@ class CustomProviderAdapter { }) if (matches === null) { - throw new Error("Custom provider returned malformed response"); + throw new Error("Custom provider returned malformed response") } // re-map keys to throw out @@ -37,13 +37,14 @@ class CustomProviderAdapter { author, narrator, publisher, - published_year, + publishedYear, description, cover, isbn, asin, genres, tags, + series, language, duration, }) => { @@ -53,13 +54,14 @@ class CustomProviderAdapter { author, narrator, publisher, - publishedYear: published_year, + publishedYear, description, cover, isbn, asin, genres, tags: tags.join(","), + series: series.length ? series : null, language, duration, } From 850397e4c1bfe2a4d13727a08169e25337cd9a3f Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 12 Jan 2024 17:58:07 -0600 Subject: [PATCH 0307/2145] Add:Playlist button to podcast episodes on latest page #2455 --- .../pages/library/_library/podcast/latest.vue | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/client/pages/library/_library/podcast/latest.vue b/client/pages/library/_library/podcast/latest.vue index e69e055f..8d95203f 100644 --- a/client/pages/library/_library/podcast/latest.vue +++ b/client/pages/library/_library/podcast/latest.vue @@ -54,9 +54,16 @@ <p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p> </button> - <button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)"> - <span class="material-icons-outlined text-2xl">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span> - </button> + <ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="playerQueueEpisodeIdMap[episode.id] ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" direction="top"> + <ui-icon-btn :icon="playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick(episode)" /> + <!-- <button class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)"> + <span class="material-icons-outlined text-2xl">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span> + </button> --> + </ui-tooltip> + + <ui-tooltip :text="$strings.LabelYourPlaylists" direction="top"> + <ui-icon-btn icon="playlist_add" borderless @click="clickAddToPlaylist(episode)" /> + </ui-tooltip> </div> </div> @@ -136,6 +143,15 @@ export default { } }, methods: { + clickAddToPlaylist(episode) { + // Makeshift libraryItem + const libraryItem = { + id: episode.libraryItemId, + media: episode.podcast + } + this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: libraryItem, episode }]) + this.$store.commit('globals/setShowPlaylistsModal', true) + }, async clickEpisode(episode) { if (this.openingItem) return this.openingItem = true From e76af3bfc2e972dbca7851c96191f1c16a10d229 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 13 Jan 2024 16:41:13 -0600 Subject: [PATCH 0308/2145] Fix comic page menu dropdown highlight correct page --- client/components/readers/ComicReader.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/readers/ComicReader.vue b/client/components/readers/ComicReader.vue index 67aa16c6..d55fc0d6 100644 --- a/client/components/readers/ComicReader.vue +++ b/client/components/readers/ComicReader.vue @@ -1,7 +1,7 @@ <template> <div class="w-full h-full"> <div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 left-8 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400" :style="{ width: pageMenuWidth + 'px' }"> - <div v-for="(file, index) in cleanedPageNames" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click="setPage(index + 1)"> + <div v-for="(file, index) in cleanedPageNames" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index + 1 ? 'bg-black-200' : ''" @click="setPage(index + 1)"> <p class="text-sm truncate">{{ file }}</p> </div> </div> From f5545cd3f45016c271f6b6c43e06a61d21091823 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 14 Jan 2024 17:51:26 -0600 Subject: [PATCH 0309/2145] Add:Scanner extracts cover from comic files #1837 and ComicInfo.xml parser --- server/libs/libarchive/LICENSE | 21 ++ server/libs/libarchive/archive.js | 262 ++++++++++++++++++ server/libs/libarchive/libarchiveWorker.js | 72 +++++ server/libs/libarchive/wasm-libarchive.js | 18 ++ server/libs/libarchive/wasm-module.js | 235 ++++++++++++++++ server/scanner/BookScanner.js | 2 +- .../utils/parsers/parseComicInfoMetadata.js | 35 +++ server/utils/parsers/parseComicMetadata.js | 109 ++++++++ server/utils/parsers/parseEbookMetadata.js | 7 +- server/utils/parsers/parseEpubMetadata.js | 5 +- 10 files changed, 762 insertions(+), 4 deletions(-) create mode 100644 server/libs/libarchive/LICENSE create mode 100644 server/libs/libarchive/archive.js create mode 100644 server/libs/libarchive/libarchiveWorker.js create mode 100644 server/libs/libarchive/wasm-libarchive.js create mode 100644 server/libs/libarchive/wasm-module.js create mode 100644 server/utils/parsers/parseComicInfoMetadata.js create mode 100644 server/utils/parsers/parseComicMetadata.js diff --git a/server/libs/libarchive/LICENSE b/server/libs/libarchive/LICENSE new file mode 100644 index 00000000..e9ec54b0 --- /dev/null +++ b/server/libs/libarchive/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 ნიკა + +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. \ No newline at end of file diff --git a/server/libs/libarchive/archive.js b/server/libs/libarchive/archive.js new file mode 100644 index 00000000..1f6b962b --- /dev/null +++ b/server/libs/libarchive/archive.js @@ -0,0 +1,262 @@ +/** + * Modified from https://github.com/nika-begiashvili/libarchivejs + */ + +const Path = require('path') +const { Worker } = require('worker_threads') + +/** + * Represents compressed file before extraction + */ +class CompressedFile { + + constructor(name, size, path, archiveRef) { + this._name = name + this._size = size + this._path = path + this._archiveRef = archiveRef + } + + /** + * file name + */ + get name() { + return this._name + } + /** + * file size + */ + get size() { + return this._size + } + + /** + * Extract file from archive + * @returns {Promise<File>} extracted file + */ + extract() { + return this._archiveRef.extractSingleFile(this._path) + } + +} + +class Archive { + /** + * Creates new archive instance from browser native File object + * @param {Buffer} fileBuffer + * @param {object} options + * @returns {Archive} + */ + static open(fileBuffer) { + const arch = new Archive(fileBuffer, { workerUrl: Path.join(__dirname, 'libarchiveWorker.js') }) + return arch.open() + } + + /** + * Create new archive + * @param {File} file + * @param {Object} options + */ + constructor(file, options) { + this._worker = new Worker(options.workerUrl) + this._worker.on('message', this._workerMsg.bind(this)) + + this._callbacks = [] + this._content = {} + this._processed = 0 + this._file = file + } + + /** + * Prepares file for reading + * @returns {Promise<Archive>} archive instance + */ + async open() { + await this._postMessage({ type: 'HELLO' }, (resolve, reject, msg) => { + if (msg.type === 'READY') { + resolve() + } + }) + return await this._postMessage({ type: 'OPEN', file: this._file }, (resolve, reject, msg) => { + if (msg.type === 'OPENED') { + resolve(this) + } + }) + } + + /** + * Terminate worker to free up memory + */ + close() { + this._worker.terminate() + this._worker = null + } + + /** + * detect if archive has encrypted data + * @returns {boolean|null} null if could not be determined + */ + hasEncryptedData() { + return this._postMessage({ type: 'CHECK_ENCRYPTION' }, + (resolve, reject, msg) => { + if (msg.type === 'ENCRYPTION_STATUS') { + resolve(msg.status) + } + } + ) + } + + /** + * set password to be used when reading archive + */ + usePassword(archivePassword) { + return this._postMessage({ type: 'SET_PASSPHRASE', passphrase: archivePassword }, + (resolve, reject, msg) => { + if (msg.type === 'PASSPHRASE_STATUS') { + resolve(msg.status) + } + } + ) + } + + /** + * Returns object containing directory structure and file information + * @returns {Promise<object>} + */ + getFilesObject() { + if (this._processed > 0) { + return Promise.resolve().then(() => this._content) + } + return this._postMessage({ type: 'LIST_FILES' }, (resolve, reject, msg) => { + if (msg.type === 'ENTRY') { + const entry = msg.entry + const [target, prop] = this._getProp(this._content, entry.path) + if (entry.type === 'FILE') { + target[prop] = new CompressedFile(entry.fileName, entry.size, entry.path, this) + } + return true + } else if (msg.type === 'END') { + this._processed = 1 + resolve(this._cloneContent(this._content)) + } + }) + } + + getFilesArray() { + return this.getFilesObject().then((obj) => { + return this._objectToArray(obj) + }) + } + + extractSingleFile(target) { + // Prevent extraction if worker already terminated + if (this._worker === null) { + throw new Error("Archive already closed") + } + + return this._postMessage({ type: 'EXTRACT_SINGLE_FILE', target: target }, + (resolve, reject, msg) => { + if (msg.type === 'FILE') { + resolve(msg.entry) + } + } + ) + } + + /** + * Returns object containing directory structure and extracted File objects + * @param {Function} extractCallback + * + */ + extractFiles(extractCallback) { + if (this._processed > 1) { + return Promise.resolve().then(() => this._content) + } + return this._postMessage({ type: 'EXTRACT_FILES' }, (resolve, reject, msg) => { + if (msg.type === 'ENTRY') { + const [target, prop] = this._getProp(this._content, msg.entry.path) + if (msg.entry.type === 'FILE') { + target[prop] = msg.entry + if (extractCallback !== undefined) { + setTimeout(extractCallback.bind(null, { + file: target[prop], + path: msg.entry.path, + })) + } + } + return true + } else if (msg.type === 'END') { + this._processed = 2 + this._worker.terminate() + resolve(this._cloneContent(this._content)) + } + }) + } + + _cloneContent(obj) { + if (obj instanceof CompressedFile || obj === null) return obj + const o = {} + for (const prop of Object.keys(obj)) { + o[prop] = this._cloneContent(obj[prop]) + } + return o + } + + _objectToArray(obj, path = '') { + const files = [] + for (const key of Object.keys(obj)) { + if (obj[key] instanceof CompressedFile || obj[key] === null) { + files.push({ + file: obj[key] || key, + path: path + }) + } else { + files.push(...this._objectToArray(obj[key], `${path}${key}/`)) + } + } + return files + } + + _getProp(obj, path) { + const parts = path.split('/') + if (parts[parts.length - 1] === '') parts.pop() + let cur = obj, prev = null + for (const part of parts) { + cur[part] = cur[part] || {} + prev = cur + cur = cur[part] + } + return [prev, parts[parts.length - 1]] + } + + _postMessage(msg, callback) { + this._worker.postMessage(msg) + return new Promise((resolve, reject) => { + this._callbacks.push(this._msgHandler.bind(this, callback, resolve, reject)) + }) + } + + _msgHandler(callback, resolve, reject, msg) { + if (!msg) { + reject('invalid msg') + return + } + if (msg.type === 'BUSY') { + reject('worker is busy') + } else if (msg.type === 'ERROR') { + reject(msg.error) + } else { + return callback(resolve, reject, msg) + } + } + + _workerMsg(msg) { + const callback = this._callbacks[this._callbacks.length - 1] + const next = callback(msg) + if (!next) { + this._callbacks.pop() + } + } + +} +module.exports = Archive \ No newline at end of file diff --git a/server/libs/libarchive/libarchiveWorker.js b/server/libs/libarchive/libarchiveWorker.js new file mode 100644 index 00000000..e768a8c2 --- /dev/null +++ b/server/libs/libarchive/libarchiveWorker.js @@ -0,0 +1,72 @@ +/** + * Modified from https://github.com/nika-begiashvili/libarchivejs + */ + +const { parentPort } = require('worker_threads') +const { getArchiveReader } = require('./wasm-module') + +let reader = null +let busy = false + +getArchiveReader((_reader) => { + reader = _reader + busy = false + parentPort.postMessage({ type: 'READY' }) +}) + +parentPort.on('message', async msg => { + if (busy) { + parentPort.postMessage({ type: 'BUSY' }) + return + } + + let skipExtraction = false + busy = true + try { + switch (msg.type) { + case 'HELLO': // module will respond READY when it's ready + break + case 'OPEN': + await reader.open(msg.file) + parentPort.postMessage({ type: 'OPENED' }) + break + case 'LIST_FILES': + skipExtraction = true + // eslint-disable-next-line no-fallthrough + case 'EXTRACT_FILES': + for (const entry of reader.entries(skipExtraction)) { + parentPort.postMessage({ type: 'ENTRY', entry }) + } + parentPort.postMessage({ type: 'END' }) + break + case 'EXTRACT_SINGLE_FILE': + for (const entry of reader.entries(true, msg.target)) { + if (entry.fileData) { + parentPort.postMessage({ type: 'FILE', entry }) + } + } + break + case 'CHECK_ENCRYPTION': + parentPort.postMessage({ type: 'ENCRYPTION_STATUS', status: reader.hasEncryptedData() }) + break + case 'SET_PASSPHRASE': + reader.setPassphrase(msg.passphrase) + parentPort.postMessage({ type: 'PASSPHRASE_STATUS', status: true }) + break + default: + throw new Error('Invalid Command') + } + } catch (err) { + parentPort.postMessage({ + type: 'ERROR', + error: { + message: err.message, + name: err.name, + stack: err.stack + } + }) + } finally { + // eslint-disable-next-line require-atomic-updates + busy = false + } +}) diff --git a/server/libs/libarchive/wasm-libarchive.js b/server/libs/libarchive/wasm-libarchive.js new file mode 100644 index 00000000..6e40d6c9 --- /dev/null +++ b/server/libs/libarchive/wasm-libarchive.js @@ -0,0 +1,18 @@ +/** + * Modified from https://github.com/nika-begiashvili/libarchivejs + */ + +var libarchive = (function () { + var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined + return ( + function (libarchive) { + libarchive = libarchive || {} + + var Module = typeof libarchive !== "undefined" ? libarchive : {}; var moduleOverrides = {}; var key; for (key in Module) { if (Module.hasOwnProperty(key)) { moduleOverrides[key] = Module[key] } } Module["arguments"] = []; Module["thisProgram"] = "./this.program"; Module["quit"] = function (status, toThrow) { throw toThrow }; Module["preRun"] = []; Module["postRun"] = []; var ENVIRONMENT_IS_WEB = false; var ENVIRONMENT_IS_WORKER = false; var ENVIRONMENT_IS_NODE = false; var ENVIRONMENT_IS_SHELL = false; ENVIRONMENT_IS_WEB = typeof window === "object"; ENVIRONMENT_IS_WORKER = typeof importScripts === "function"; ENVIRONMENT_IS_NODE = typeof process === "object" && typeof require === "function" && !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_WORKER; ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; var scriptDirectory = ""; function locateFile(path) { if (Module["locateFile"]) { return Module["locateFile"](path, scriptDirectory) } else { return scriptDirectory + path } } if (ENVIRONMENT_IS_NODE) { scriptDirectory = __dirname + "/"; var nodeFS; var nodePath; Module["read"] = function shell_read(filename, binary) { var ret; if (!nodeFS) nodeFS = require("fs"); if (!nodePath) nodePath = require("path"); filename = nodePath["normalize"](filename); ret = nodeFS["readFileSync"](filename); return binary ? ret : ret.toString() }; Module["readBinary"] = function readBinary(filename) { var ret = Module["read"](filename, true); if (!ret.buffer) { ret = new Uint8Array(ret) } assert(ret.buffer); return ret }; if (process["argv"].length > 1) { Module["thisProgram"] = process["argv"][1].replace(/\\/g, "/") } Module["arguments"] = process["argv"].slice(2); process["on"]("uncaughtException", function (ex) { if (!(ex instanceof ExitStatus)) { throw ex } }); process["on"]("unhandledRejection", abort); Module["quit"] = function (status) { process["exit"](status) }; Module["inspect"] = function () { return "[Emscripten Module object]" } } else if (ENVIRONMENT_IS_SHELL) { if (typeof read != "undefined") { Module["read"] = function shell_read(f) { return read(f) } } Module["readBinary"] = function readBinary(f) { var data; if (typeof readbuffer === "function") { return new Uint8Array(readbuffer(f)) } data = read(f, "binary"); assert(typeof data === "object"); return data }; if (typeof scriptArgs != "undefined") { Module["arguments"] = scriptArgs } else if (typeof arguments != "undefined") { Module["arguments"] = arguments } if (typeof quit === "function") { Module["quit"] = function (status) { quit(status) } } } else if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { if (ENVIRONMENT_IS_WORKER) { scriptDirectory = self.location.href } else if (document.currentScript) { scriptDirectory = document.currentScript.src } if (_scriptDir) { scriptDirectory = _scriptDir } if (scriptDirectory.indexOf("blob:") !== 0) { scriptDirectory = scriptDirectory.substr(0, scriptDirectory.lastIndexOf("/") + 1) } else { scriptDirectory = "" } Module["read"] = function shell_read(url) { var xhr = new XMLHttpRequest; xhr.open("GET", url, false); xhr.send(null); return xhr.responseText }; if (ENVIRONMENT_IS_WORKER) { Module["readBinary"] = function readBinary(url) { var xhr = new XMLHttpRequest; xhr.open("GET", url, false); xhr.responseType = "arraybuffer"; xhr.send(null); return new Uint8Array(xhr.response) } } Module["readAsync"] = function readAsync(url, onload, onerror) { var xhr = new XMLHttpRequest; xhr.open("GET", url, true); xhr.responseType = "arraybuffer"; xhr.onload = function xhr_onload() { if (xhr.status == 200 || xhr.status == 0 && xhr.response) { onload(xhr.response); return } onerror() }; xhr.onerror = onerror; xhr.send(null) }; Module["setWindowTitle"] = function (title) { document.title = title } } else { } var out = Module["print"] || (typeof console !== "undefined" ? console.log.bind(console) : typeof print !== "undefined" ? print : null); var err = Module["printErr"] || (typeof printErr !== "undefined" ? printErr : typeof console !== "undefined" && console.warn.bind(console) || out); for (key in moduleOverrides) { if (moduleOverrides.hasOwnProperty(key)) { Module[key] = moduleOverrides[key] } } moduleOverrides = undefined; function dynamicAlloc(size) { var ret = HEAP32[DYNAMICTOP_PTR >> 2]; var end = ret + size + 15 & -16; if (end <= _emscripten_get_heap_size()) { HEAP32[DYNAMICTOP_PTR >> 2] = end } else { var success = _emscripten_resize_heap(end); if (!success) return 0 } return ret } function getNativeTypeSize(type) { switch (type) { case "i1": case "i8": return 1; case "i16": return 2; case "i32": return 4; case "i64": return 8; case "float": return 4; case "double": return 8; default: { if (type[type.length - 1] === "*") { return 4 } else if (type[0] === "i") { var bits = parseInt(type.substr(1)); assert(bits % 8 === 0, "getNativeTypeSize invalid bits " + bits + ", type " + type); return bits / 8 } else { return 0 } } } } var asm2wasmImports = { "f64-rem": function (x, y) { return x % y }, "debugger": function () { debugger } }; var functionPointers = new Array(0); var tempRet0 = 0; var setTempRet0 = function (value) { tempRet0 = value }; if (typeof WebAssembly !== "object") { err("no native wasm support detected") } var wasmMemory; var wasmTable; var ABORT = false; var EXITSTATUS = 0; function assert(condition, text) { if (!condition) { abort("Assertion failed: " + text) } } function getCFunc(ident) { var func = Module["_" + ident]; assert(func, "Cannot call unknown function " + ident + ", make sure it is exported"); return func } function ccall(ident, returnType, argTypes, args, opts) { var toC = { "string": function (str) { var ret = 0; if (str !== null && str !== undefined && str !== 0) { var len = (str.length << 2) + 1; ret = stackAlloc(len); stringToUTF8(str, ret, len) } return ret }, "array": function (arr) { var ret = stackAlloc(arr.length); writeArrayToMemory(arr, ret); return ret } }; function convertReturnValue(ret) { if (returnType === "string") return UTF8ToString(ret); if (returnType === "boolean") return Boolean(ret); return ret } var func = getCFunc(ident); var cArgs = []; var stack = 0; if (args) { for (var i = 0; i < args.length; i++) { var converter = toC[argTypes[i]]; if (converter) { if (stack === 0) stack = stackSave(); cArgs[i] = converter(args[i]) } else { cArgs[i] = args[i] } } } var ret = func.apply(null, cArgs); ret = convertReturnValue(ret); if (stack !== 0) stackRestore(stack); return ret } function cwrap(ident, returnType, argTypes, opts) { argTypes = argTypes || []; var numericArgs = argTypes.every(function (type) { return type === "number" }); var numericRet = returnType !== "string"; if (numericRet && numericArgs && !opts) { return getCFunc(ident) } return function () { return ccall(ident, returnType, argTypes, arguments, opts) } } function setValue(ptr, value, type, noSafe) { type = type || "i8"; if (type.charAt(type.length - 1) === "*") type = "i32"; switch (type) { case "i1": HEAP8[ptr >> 0] = value; break; case "i8": HEAP8[ptr >> 0] = value; break; case "i16": HEAP16[ptr >> 1] = value; break; case "i32": HEAP32[ptr >> 2] = value; break; case "i64": tempI64 = [value >>> 0, (tempDouble = value, +Math_abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0)], HEAP32[ptr >> 2] = tempI64[0], HEAP32[ptr + 4 >> 2] = tempI64[1]; break; case "float": HEAPF32[ptr >> 2] = value; break; case "double": HEAPF64[ptr >> 3] = value; break; default: abort("invalid type for setValue: " + type) } } var ALLOC_NORMAL = 0; var ALLOC_NONE = 3; function allocate(slab, types, allocator, ptr) { var zeroinit, size; if (typeof slab === "number") { zeroinit = true; size = slab } else { zeroinit = false; size = slab.length } var singleType = typeof types === "string" ? types : null; var ret; if (allocator == ALLOC_NONE) { ret = ptr } else { ret = [_malloc, stackAlloc, dynamicAlloc][allocator](Math.max(size, singleType ? 1 : types.length)) } if (zeroinit) { var stop; ptr = ret; assert((ret & 3) == 0); stop = ret + (size & ~3); for (; ptr < stop; ptr += 4) { HEAP32[ptr >> 2] = 0 } stop = ret + size; while (ptr < stop) { HEAP8[ptr++ >> 0] = 0 } return ret } if (singleType === "i8") { if (slab.subarray || slab.slice) { HEAPU8.set(slab, ret) } else { HEAPU8.set(new Uint8Array(slab), ret) } return ret } var i = 0, type, typeSize, previousType; while (i < size) { var curr = slab[i]; type = singleType || types[i]; if (type === 0) { i++; continue } if (type == "i64") type = "i32"; setValue(ret + i, curr, type); if (previousType !== type) { typeSize = getNativeTypeSize(type); previousType = type } i += typeSize } return ret } function getMemory(size) { if (!runtimeInitialized) return dynamicAlloc(size); return _malloc(size) } var UTF8Decoder = typeof TextDecoder !== "undefined" ? new TextDecoder("utf8") : undefined; function UTF8ArrayToString(u8Array, idx, maxBytesToRead) { var endIdx = idx + maxBytesToRead; var endPtr = idx; while (u8Array[endPtr] && !(endPtr >= endIdx)) ++endPtr; if (endPtr - idx > 16 && u8Array.subarray && UTF8Decoder) { return UTF8Decoder.decode(u8Array.subarray(idx, endPtr)) } else { var str = ""; while (idx < endPtr) { var u0 = u8Array[idx++]; if (!(u0 & 128)) { str += String.fromCharCode(u0); continue } var u1 = u8Array[idx++] & 63; if ((u0 & 224) == 192) { str += String.fromCharCode((u0 & 31) << 6 | u1); continue } var u2 = u8Array[idx++] & 63; if ((u0 & 240) == 224) { u0 = (u0 & 15) << 12 | u1 << 6 | u2 } else { u0 = (u0 & 7) << 18 | u1 << 12 | u2 << 6 | u8Array[idx++] & 63 } if (u0 < 65536) { str += String.fromCharCode(u0) } else { var ch = u0 - 65536; str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023) } } } return str } function UTF8ToString(ptr, maxBytesToRead) { return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : "" } function stringToUTF8Array(str, outU8Array, outIdx, maxBytesToWrite) { if (!(maxBytesToWrite > 0)) return 0; var startIdx = outIdx; var endIdx = outIdx + maxBytesToWrite - 1; for (var i = 0; i < str.length; ++i) { var u = str.charCodeAt(i); if (u >= 55296 && u <= 57343) { var u1 = str.charCodeAt(++i); u = 65536 + ((u & 1023) << 10) | u1 & 1023 } if (u <= 127) { if (outIdx >= endIdx) break; outU8Array[outIdx++] = u } else if (u <= 2047) { if (outIdx + 1 >= endIdx) break; outU8Array[outIdx++] = 192 | u >> 6; outU8Array[outIdx++] = 128 | u & 63 } else if (u <= 65535) { if (outIdx + 2 >= endIdx) break; outU8Array[outIdx++] = 224 | u >> 12; outU8Array[outIdx++] = 128 | u >> 6 & 63; outU8Array[outIdx++] = 128 | u & 63 } else { if (outIdx + 3 >= endIdx) break; outU8Array[outIdx++] = 240 | u >> 18; outU8Array[outIdx++] = 128 | u >> 12 & 63; outU8Array[outIdx++] = 128 | u >> 6 & 63; outU8Array[outIdx++] = 128 | u & 63 } } outU8Array[outIdx] = 0; return outIdx - startIdx } function stringToUTF8(str, outPtr, maxBytesToWrite) { return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite) } function lengthBytesUTF8(str) { var len = 0; for (var i = 0; i < str.length; ++i) { var u = str.charCodeAt(i); if (u >= 55296 && u <= 57343) u = 65536 + ((u & 1023) << 10) | str.charCodeAt(++i) & 1023; if (u <= 127) ++len; else if (u <= 2047) len += 2; else if (u <= 65535) len += 3; else len += 4 } return len } var UTF16Decoder = typeof TextDecoder !== "undefined" ? new TextDecoder("utf-16le") : undefined; function writeArrayToMemory(array, buffer) { HEAP8.set(array, buffer) } function writeAsciiToMemory(str, buffer, dontAddNull) { for (var i = 0; i < str.length; ++i) { HEAP8[buffer++ >> 0] = str.charCodeAt(i) } if (!dontAddNull) HEAP8[buffer >> 0] = 0 } function demangle(func) { return func } function demangleAll(text) { var regex = /__Z[\w\d_]+/g; return text.replace(regex, function (x) { var y = demangle(x); return x === y ? x : y + " [" + x + "]" }) } function jsStackTrace() { var err = new Error; if (!err.stack) { try { throw new Error(0) } catch (e) { err = e } if (!err.stack) { return "(no stack trace available)" } } return err.stack.toString() } function stackTrace() { var js = jsStackTrace(); if (Module["extraStackTrace"]) js += "\n" + Module["extraStackTrace"](); return demangleAll(js) } var WASM_PAGE_SIZE = 65536; function alignUp(x, multiple) { if (x % multiple > 0) { x += multiple - x % multiple } return x } var buffer, HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAPF64; function updateGlobalBufferViews() { Module["HEAP8"] = HEAP8 = new Int8Array(buffer); Module["HEAP16"] = HEAP16 = new Int16Array(buffer); Module["HEAP32"] = HEAP32 = new Int32Array(buffer); Module["HEAPU8"] = HEAPU8 = new Uint8Array(buffer); Module["HEAPU16"] = HEAPU16 = new Uint16Array(buffer); Module["HEAPU32"] = HEAPU32 = new Uint32Array(buffer); Module["HEAPF32"] = HEAPF32 = new Float32Array(buffer); Module["HEAPF64"] = HEAPF64 = new Float64Array(buffer) } var DYNAMIC_BASE = 5520464, DYNAMICTOP_PTR = 277552; var TOTAL_STACK = 5242880; var INITIAL_TOTAL_MEMORY = Module["TOTAL_MEMORY"] || 16777216; if (INITIAL_TOTAL_MEMORY < TOTAL_STACK) err("TOTAL_MEMORY should be larger than TOTAL_STACK, was " + INITIAL_TOTAL_MEMORY + "! (TOTAL_STACK=" + TOTAL_STACK + ")"); if (Module["buffer"]) { buffer = Module["buffer"] } else { if (typeof WebAssembly === "object" && typeof WebAssembly.Memory === "function") { wasmMemory = new WebAssembly.Memory({ "initial": INITIAL_TOTAL_MEMORY / WASM_PAGE_SIZE }); buffer = wasmMemory.buffer } else { buffer = new ArrayBuffer(INITIAL_TOTAL_MEMORY) } } updateGlobalBufferViews(); HEAP32[DYNAMICTOP_PTR >> 2] = DYNAMIC_BASE; function callRuntimeCallbacks(callbacks) { while (callbacks.length > 0) { var callback = callbacks.shift(); if (typeof callback == "function") { callback(); continue } var func = callback.func; if (typeof func === "number") { if (callback.arg === undefined) { Module["dynCall_v"](func) } else { Module["dynCall_vi"](func, callback.arg) } } else { func(callback.arg === undefined ? null : callback.arg) } } } var __ATPRERUN__ = []; var __ATINIT__ = []; var __ATMAIN__ = []; var __ATPOSTRUN__ = []; var runtimeInitialized = false; var runtimeExited = false; function preRun() { if (Module["preRun"]) { if (typeof Module["preRun"] == "function") Module["preRun"] = [Module["preRun"]]; while (Module["preRun"].length) { addOnPreRun(Module["preRun"].shift()) } } callRuntimeCallbacks(__ATPRERUN__) } function ensureInitRuntime() { if (runtimeInitialized) return; runtimeInitialized = true; if (!Module["noFSInit"] && !FS.init.initialized) FS.init(); TTY.init(); PIPEFS.root = FS.mount(PIPEFS, {}, null); callRuntimeCallbacks(__ATINIT__) } function preMain() { FS.ignorePermissions = false; callRuntimeCallbacks(__ATMAIN__) } function exitRuntime() { runtimeExited = true } function postRun() { if (Module["postRun"]) { if (typeof Module["postRun"] == "function") Module["postRun"] = [Module["postRun"]]; while (Module["postRun"].length) { addOnPostRun(Module["postRun"].shift()) } } callRuntimeCallbacks(__ATPOSTRUN__) } function addOnPreRun(cb) { __ATPRERUN__.unshift(cb) } function addOnPostRun(cb) { __ATPOSTRUN__.unshift(cb) } var Math_abs = Math.abs; var Math_ceil = Math.ceil; var Math_floor = Math.floor; var Math_min = Math.min; var runDependencies = 0; var runDependencyWatcher = null; var dependenciesFulfilled = null; function getUniqueRunDependency(id) { return id } function addRunDependency(id) { runDependencies++; if (Module["monitorRunDependencies"]) { Module["monitorRunDependencies"](runDependencies) } } function removeRunDependency(id) { runDependencies--; if (Module["monitorRunDependencies"]) { Module["monitorRunDependencies"](runDependencies) } if (runDependencies == 0) { if (runDependencyWatcher !== null) { clearInterval(runDependencyWatcher); runDependencyWatcher = null } if (dependenciesFulfilled) { var callback = dependenciesFulfilled; dependenciesFulfilled = null; callback() } } } Module["preloadedImages"] = {}; Module["preloadedAudios"] = {}; var dataURIPrefix = "data:application/octet-stream;base64,"; function isDataURI(filename) { return String.prototype.startsWith ? filename.startsWith(dataURIPrefix) : filename.indexOf(dataURIPrefix) === 0 } var wasmBinaryFile = "libarchive.wasm"; if (!isDataURI(wasmBinaryFile)) { wasmBinaryFile = locateFile(wasmBinaryFile) } function getBinary() { try { if (Module["wasmBinary"]) { return new Uint8Array(Module["wasmBinary"]) } if (Module["readBinary"]) { return Module["readBinary"](wasmBinaryFile) } else { throw "both async and sync fetching of the wasm failed" } } catch (err) { abort(err) } } function getBinaryPromise() { if (!Module["wasmBinary"] && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) && typeof fetch === "function") { return fetch(wasmBinaryFile, { credentials: "same-origin" }).then(function (response) { if (!response["ok"]) { throw "failed to load wasm binary file at '" + wasmBinaryFile + "'" } return response["arrayBuffer"]() }).catch(function () { return getBinary() }) } return new Promise(function (resolve, reject) { resolve(getBinary()) }) } function createWasm(env) { var info = { "env": env, "global": { "NaN": NaN, Infinity: Infinity }, "global.Math": Math, "asm2wasm": asm2wasmImports }; function receiveInstance(instance, module) { var exports = instance.exports; Module["asm"] = exports; removeRunDependency("wasm-instantiate") } addRunDependency("wasm-instantiate"); if (Module["instantiateWasm"]) { try { return Module["instantiateWasm"](info, receiveInstance) } catch (e) { err("Module.instantiateWasm callback failed with error: " + e); return false } } function receiveInstantiatedSource(output) { receiveInstance(output["instance"]) } function instantiateArrayBuffer(receiver) { getBinaryPromise().then(function (binary) { return WebAssembly.instantiate(binary, info) }).then(receiver, function (reason) { err("failed to asynchronously prepare wasm: " + reason); abort(reason) }) } if (!Module["wasmBinary"] && typeof WebAssembly.instantiateStreaming === "function" && !isDataURI(wasmBinaryFile) && typeof fetch === "function") { WebAssembly.instantiateStreaming(fetch(wasmBinaryFile, { credentials: "same-origin" }), info).then(receiveInstantiatedSource, function (reason) { err("wasm streaming compile failed: " + reason); err("falling back to ArrayBuffer instantiation"); instantiateArrayBuffer(receiveInstantiatedSource) }) } else { instantiateArrayBuffer(receiveInstantiatedSource) } return {} } Module["asm"] = function (global, env, providedBuffer) { env["memory"] = wasmMemory; env["table"] = wasmTable = new WebAssembly.Table({ "initial": 507, "maximum": 507, "element": "anyfunc" }); env["__memory_base"] = 1024; env["__table_base"] = 0; var exports = createWasm(env); return exports }; __ATINIT__.push({ func: function () { ___emscripten_environ_constructor() } }); var ENV = {}; function ___buildEnvironment(environ) { var MAX_ENV_VALUES = 64; var TOTAL_ENV_SIZE = 1024; var poolPtr; var envPtr; if (!___buildEnvironment.called) { ___buildEnvironment.called = true; ENV["USER"] = ENV["LOGNAME"] = "web_user"; ENV["PATH"] = "/"; ENV["PWD"] = "/"; ENV["HOME"] = "/home/web_user"; ENV["LANG"] = "C.UTF-8"; ENV["_"] = Module["thisProgram"]; poolPtr = getMemory(TOTAL_ENV_SIZE); envPtr = getMemory(MAX_ENV_VALUES * 4); HEAP32[envPtr >> 2] = poolPtr; HEAP32[environ >> 2] = envPtr } else { envPtr = HEAP32[environ >> 2]; poolPtr = HEAP32[envPtr >> 2] } var strings = []; var totalSize = 0; for (var key in ENV) { if (typeof ENV[key] === "string") { var line = key + "=" + ENV[key]; strings.push(line); totalSize += line.length } } if (totalSize > TOTAL_ENV_SIZE) { throw new Error("Environment size exceeded TOTAL_ENV_SIZE!") } var ptrSize = 4; for (var i = 0; i < strings.length; i++) { var line = strings[i]; writeAsciiToMemory(line, poolPtr); HEAP32[envPtr + i * ptrSize >> 2] = poolPtr; poolPtr += line.length + 1 } HEAP32[envPtr + strings.length * ptrSize >> 2] = 0 } var PATH = { splitPath: function (filename) { var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; return splitPathRe.exec(filename).slice(1) }, normalizeArray: function (parts, allowAboveRoot) { var up = 0; for (var i = parts.length - 1; i >= 0; i--) { var last = parts[i]; if (last === ".") { parts.splice(i, 1) } else if (last === "..") { parts.splice(i, 1); up++ } else if (up) { parts.splice(i, 1); up-- } } if (allowAboveRoot) { for (; up; up--) { parts.unshift("..") } } return parts }, normalize: function (path) { var isAbsolute = path.charAt(0) === "/", trailingSlash = path.substr(-1) === "/"; path = PATH.normalizeArray(path.split("/").filter(function (p) { return !!p }), !isAbsolute).join("/"); if (!path && !isAbsolute) { path = "." } if (path && trailingSlash) { path += "/" } return (isAbsolute ? "/" : "") + path }, dirname: function (path) { var result = PATH.splitPath(path), root = result[0], dir = result[1]; if (!root && !dir) { return "." } if (dir) { dir = dir.substr(0, dir.length - 1) } return root + dir }, basename: function (path) { if (path === "/") return "/"; var lastSlash = path.lastIndexOf("/"); if (lastSlash === -1) return path; return path.substr(lastSlash + 1) }, extname: function (path) { return PATH.splitPath(path)[3] }, join: function () { var paths = Array.prototype.slice.call(arguments, 0); return PATH.normalize(paths.join("/")) }, join2: function (l, r) { return PATH.normalize(l + "/" + r) } }; function ___setErrNo(value) { if (Module["___errno_location"]) HEAP32[Module["___errno_location"]() >> 2] = value; return value } var PATH_FS = { resolve: function () { var resolvedPath = "", resolvedAbsolute = false; for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { var path = i >= 0 ? arguments[i] : FS.cwd(); if (typeof path !== "string") { throw new TypeError("Arguments to path.resolve must be strings") } else if (!path) { return "" } resolvedPath = path + "/" + resolvedPath; resolvedAbsolute = path.charAt(0) === "/" } resolvedPath = PATH.normalizeArray(resolvedPath.split("/").filter(function (p) { return !!p }), !resolvedAbsolute).join("/"); return (resolvedAbsolute ? "/" : "") + resolvedPath || "." }, relative: function (from, to) { from = PATH_FS.resolve(from).substr(1); to = PATH_FS.resolve(to).substr(1); function trim(arr) { var start = 0; for (; start < arr.length; start++) { if (arr[start] !== "") break } var end = arr.length - 1; for (; end >= 0; end--) { if (arr[end] !== "") break } if (start > end) return []; return arr.slice(start, end - start + 1) } var fromParts = trim(from.split("/")); var toParts = trim(to.split("/")); var length = Math.min(fromParts.length, toParts.length); var samePartsLength = length; for (var i = 0; i < length; i++) { if (fromParts[i] !== toParts[i]) { samePartsLength = i; break } } var outputParts = []; for (var i = samePartsLength; i < fromParts.length; i++) { outputParts.push("..") } outputParts = outputParts.concat(toParts.slice(samePartsLength)); return outputParts.join("/") } }; var TTY = { ttys: [], init: function () { }, shutdown: function () { }, register: function (dev, ops) { TTY.ttys[dev] = { input: [], output: [], ops: ops }; FS.registerDevice(dev, TTY.stream_ops) }, stream_ops: { open: function (stream) { var tty = TTY.ttys[stream.node.rdev]; if (!tty) { throw new FS.ErrnoError(19) } stream.tty = tty; stream.seekable = false }, close: function (stream) { stream.tty.ops.flush(stream.tty) }, flush: function (stream) { stream.tty.ops.flush(stream.tty) }, read: function (stream, buffer, offset, length, pos) { if (!stream.tty || !stream.tty.ops.get_char) { throw new FS.ErrnoError(6) } var bytesRead = 0; for (var i = 0; i < length; i++) { var result; try { result = stream.tty.ops.get_char(stream.tty) } catch (e) { throw new FS.ErrnoError(5) } if (result === undefined && bytesRead === 0) { throw new FS.ErrnoError(11) } if (result === null || result === undefined) break; bytesRead++; buffer[offset + i] = result } if (bytesRead) { stream.node.timestamp = Date.now() } return bytesRead }, write: function (stream, buffer, offset, length, pos) { if (!stream.tty || !stream.tty.ops.put_char) { throw new FS.ErrnoError(6) } try { for (var i = 0; i < length; i++) { stream.tty.ops.put_char(stream.tty, buffer[offset + i]) } } catch (e) { throw new FS.ErrnoError(5) } if (length) { stream.node.timestamp = Date.now() } return i } }, default_tty_ops: { get_char: function (tty) { if (!tty.input.length) { var result = null; if (ENVIRONMENT_IS_NODE) { var BUFSIZE = 256; var buf = new Buffer(BUFSIZE); var bytesRead = 0; var isPosixPlatform = process.platform != "win32"; var fd = process.stdin.fd; if (isPosixPlatform) { var usingDevice = false; try { fd = fs.openSync("/dev/stdin", "r"); usingDevice = true } catch (e) { } } try { bytesRead = fs.readSync(fd, buf, 0, BUFSIZE, null) } catch (e) { if (e.toString().indexOf("EOF") != -1) bytesRead = 0; else throw e } if (usingDevice) { fs.closeSync(fd) } if (bytesRead > 0) { result = buf.slice(0, bytesRead).toString("utf-8") } else { result = null } } else if (typeof window != "undefined" && typeof window.prompt == "function") { result = window.prompt("Input: "); if (result !== null) { result += "\n" } } else if (typeof readline == "function") { result = readline(); if (result !== null) { result += "\n" } } if (!result) { return null } tty.input = intArrayFromString(result, true) } return tty.input.shift() }, put_char: function (tty, val) { if (val === null || val === 10) { out(UTF8ArrayToString(tty.output, 0)); tty.output = [] } else { if (val != 0) tty.output.push(val) } }, flush: function (tty) { if (tty.output && tty.output.length > 0) { out(UTF8ArrayToString(tty.output, 0)); tty.output = [] } } }, default_tty1_ops: { put_char: function (tty, val) { if (val === null || val === 10) { err(UTF8ArrayToString(tty.output, 0)); tty.output = [] } else { if (val != 0) tty.output.push(val) } }, flush: function (tty) { if (tty.output && tty.output.length > 0) { err(UTF8ArrayToString(tty.output, 0)); tty.output = [] } } } }; var MEMFS = { ops_table: null, mount: function (mount) { return MEMFS.createNode(null, "/", 16384 | 511, 0) }, createNode: function (parent, name, mode, dev) { if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { throw new FS.ErrnoError(1) } if (!MEMFS.ops_table) { MEMFS.ops_table = { dir: { node: { getattr: MEMFS.node_ops.getattr, setattr: MEMFS.node_ops.setattr, lookup: MEMFS.node_ops.lookup, mknod: MEMFS.node_ops.mknod, rename: MEMFS.node_ops.rename, unlink: MEMFS.node_ops.unlink, rmdir: MEMFS.node_ops.rmdir, readdir: MEMFS.node_ops.readdir, symlink: MEMFS.node_ops.symlink }, stream: { llseek: MEMFS.stream_ops.llseek } }, file: { node: { getattr: MEMFS.node_ops.getattr, setattr: MEMFS.node_ops.setattr }, stream: { llseek: MEMFS.stream_ops.llseek, read: MEMFS.stream_ops.read, write: MEMFS.stream_ops.write, allocate: MEMFS.stream_ops.allocate, mmap: MEMFS.stream_ops.mmap, msync: MEMFS.stream_ops.msync } }, link: { node: { getattr: MEMFS.node_ops.getattr, setattr: MEMFS.node_ops.setattr, readlink: MEMFS.node_ops.readlink }, stream: {} }, chrdev: { node: { getattr: MEMFS.node_ops.getattr, setattr: MEMFS.node_ops.setattr }, stream: FS.chrdev_stream_ops } } } var node = FS.createNode(parent, name, mode, dev); if (FS.isDir(node.mode)) { node.node_ops = MEMFS.ops_table.dir.node; node.stream_ops = MEMFS.ops_table.dir.stream; node.contents = {} } else if (FS.isFile(node.mode)) { node.node_ops = MEMFS.ops_table.file.node; node.stream_ops = MEMFS.ops_table.file.stream; node.usedBytes = 0; node.contents = null } else if (FS.isLink(node.mode)) { node.node_ops = MEMFS.ops_table.link.node; node.stream_ops = MEMFS.ops_table.link.stream } else if (FS.isChrdev(node.mode)) { node.node_ops = MEMFS.ops_table.chrdev.node; node.stream_ops = MEMFS.ops_table.chrdev.stream } node.timestamp = Date.now(); if (parent) { parent.contents[name] = node } return node }, getFileDataAsRegularArray: function (node) { if (node.contents && node.contents.subarray) { var arr = []; for (var i = 0; i < node.usedBytes; ++i)arr.push(node.contents[i]); return arr } return node.contents }, getFileDataAsTypedArray: function (node) { if (!node.contents) return new Uint8Array; if (node.contents.subarray) return node.contents.subarray(0, node.usedBytes); return new Uint8Array(node.contents) }, expandFileStorage: function (node, newCapacity) { var prevCapacity = node.contents ? node.contents.length : 0; if (prevCapacity >= newCapacity) return; var CAPACITY_DOUBLING_MAX = 1024 * 1024; newCapacity = Math.max(newCapacity, prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2 : 1.125) | 0); if (prevCapacity != 0) newCapacity = Math.max(newCapacity, 256); var oldContents = node.contents; node.contents = new Uint8Array(newCapacity); if (node.usedBytes > 0) node.contents.set(oldContents.subarray(0, node.usedBytes), 0); return }, resizeFileStorage: function (node, newSize) { if (node.usedBytes == newSize) return; if (newSize == 0) { node.contents = null; node.usedBytes = 0; return } if (!node.contents || node.contents.subarray) { var oldContents = node.contents; node.contents = new Uint8Array(new ArrayBuffer(newSize)); if (oldContents) { node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))) } node.usedBytes = newSize; return } if (!node.contents) node.contents = []; if (node.contents.length > newSize) node.contents.length = newSize; else while (node.contents.length < newSize) node.contents.push(0); node.usedBytes = newSize }, node_ops: { getattr: function (node) { var attr = {}; attr.dev = FS.isChrdev(node.mode) ? node.id : 1; attr.ino = node.id; attr.mode = node.mode; attr.nlink = 1; attr.uid = 0; attr.gid = 0; attr.rdev = node.rdev; if (FS.isDir(node.mode)) { attr.size = 4096 } else if (FS.isFile(node.mode)) { attr.size = node.usedBytes } else if (FS.isLink(node.mode)) { attr.size = node.link.length } else { attr.size = 0 } attr.atime = new Date(node.timestamp); attr.mtime = new Date(node.timestamp); attr.ctime = new Date(node.timestamp); attr.blksize = 4096; attr.blocks = Math.ceil(attr.size / attr.blksize); return attr }, setattr: function (node, attr) { if (attr.mode !== undefined) { node.mode = attr.mode } if (attr.timestamp !== undefined) { node.timestamp = attr.timestamp } if (attr.size !== undefined) { MEMFS.resizeFileStorage(node, attr.size) } }, lookup: function (parent, name) { throw FS.genericErrors[2] }, mknod: function (parent, name, mode, dev) { return MEMFS.createNode(parent, name, mode, dev) }, rename: function (old_node, new_dir, new_name) { if (FS.isDir(old_node.mode)) { var new_node; try { new_node = FS.lookupNode(new_dir, new_name) } catch (e) { } if (new_node) { for (var i in new_node.contents) { throw new FS.ErrnoError(39) } } } delete old_node.parent.contents[old_node.name]; old_node.name = new_name; new_dir.contents[new_name] = old_node; old_node.parent = new_dir }, unlink: function (parent, name) { delete parent.contents[name] }, rmdir: function (parent, name) { var node = FS.lookupNode(parent, name); for (var i in node.contents) { throw new FS.ErrnoError(39) } delete parent.contents[name] }, readdir: function (node) { var entries = [".", ".."]; for (var key in node.contents) { if (!node.contents.hasOwnProperty(key)) { continue } entries.push(key) } return entries }, symlink: function (parent, newname, oldpath) { var node = MEMFS.createNode(parent, newname, 511 | 40960, 0); node.link = oldpath; return node }, readlink: function (node) { if (!FS.isLink(node.mode)) { throw new FS.ErrnoError(22) } return node.link } }, stream_ops: { read: function (stream, buffer, offset, length, position) { var contents = stream.node.contents; if (position >= stream.node.usedBytes) return 0; var size = Math.min(stream.node.usedBytes - position, length); if (size > 8 && contents.subarray) { buffer.set(contents.subarray(position, position + size), offset) } else { for (var i = 0; i < size; i++)buffer[offset + i] = contents[position + i] } return size }, write: function (stream, buffer, offset, length, position, canOwn) { canOwn = false; if (!length) return 0; var node = stream.node; node.timestamp = Date.now(); if (buffer.subarray && (!node.contents || node.contents.subarray)) { if (canOwn) { node.contents = buffer.subarray(offset, offset + length); node.usedBytes = length; return length } else if (node.usedBytes === 0 && position === 0) { node.contents = new Uint8Array(buffer.subarray(offset, offset + length)); node.usedBytes = length; return length } else if (position + length <= node.usedBytes) { node.contents.set(buffer.subarray(offset, offset + length), position); return length } } MEMFS.expandFileStorage(node, position + length); if (node.contents.subarray && buffer.subarray) node.contents.set(buffer.subarray(offset, offset + length), position); else { for (var i = 0; i < length; i++) { node.contents[position + i] = buffer[offset + i] } } node.usedBytes = Math.max(node.usedBytes, position + length); return length }, llseek: function (stream, offset, whence) { var position = offset; if (whence === 1) { position += stream.position } else if (whence === 2) { if (FS.isFile(stream.node.mode)) { position += stream.node.usedBytes } } if (position < 0) { throw new FS.ErrnoError(22) } return position }, allocate: function (stream, offset, length) { MEMFS.expandFileStorage(stream.node, offset + length); stream.node.usedBytes = Math.max(stream.node.usedBytes, offset + length) }, mmap: function (stream, buffer, offset, length, position, prot, flags) { if (!FS.isFile(stream.node.mode)) { throw new FS.ErrnoError(19) } var ptr; var allocated; var contents = stream.node.contents; if (!(flags & 2) && (contents.buffer === buffer || contents.buffer === buffer.buffer)) { allocated = false; ptr = contents.byteOffset } else { if (position > 0 || position + length < stream.node.usedBytes) { if (contents.subarray) { contents = contents.subarray(position, position + length) } else { contents = Array.prototype.slice.call(contents, position, position + length) } } allocated = true; ptr = _malloc(length); if (!ptr) { throw new FS.ErrnoError(12) } buffer.set(contents, ptr) } return { ptr: ptr, allocated: allocated } }, msync: function (stream, buffer, offset, length, mmapFlags) { if (!FS.isFile(stream.node.mode)) { throw new FS.ErrnoError(19) } if (mmapFlags & 2) { return 0 } var bytesWritten = MEMFS.stream_ops.write(stream, buffer, 0, length, offset, false); return 0 } } }; var IDBFS = { dbs: {}, indexedDB: function () { if (typeof indexedDB !== "undefined") return indexedDB; var ret = null; if (typeof window === "object") ret = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; assert(ret, "IDBFS used, but indexedDB not supported"); return ret }, DB_VERSION: 21, DB_STORE_NAME: "FILE_DATA", mount: function (mount) { return MEMFS.mount.apply(null, arguments) }, syncfs: function (mount, populate, callback) { IDBFS.getLocalSet(mount, function (err, local) { if (err) return callback(err); IDBFS.getRemoteSet(mount, function (err, remote) { if (err) return callback(err); var src = populate ? remote : local; var dst = populate ? local : remote; IDBFS.reconcile(src, dst, callback) }) }) }, getDB: function (name, callback) { var db = IDBFS.dbs[name]; if (db) { return callback(null, db) } var req; try { req = IDBFS.indexedDB().open(name, IDBFS.DB_VERSION) } catch (e) { return callback(e) } if (!req) { return callback("Unable to connect to IndexedDB") } req.onupgradeneeded = function (e) { var db = e.target.result; var transaction = e.target.transaction; var fileStore; if (db.objectStoreNames.contains(IDBFS.DB_STORE_NAME)) { fileStore = transaction.objectStore(IDBFS.DB_STORE_NAME) } else { fileStore = db.createObjectStore(IDBFS.DB_STORE_NAME) } if (!fileStore.indexNames.contains("timestamp")) { fileStore.createIndex("timestamp", "timestamp", { unique: false }) } }; req.onsuccess = function () { db = req.result; IDBFS.dbs[name] = db; callback(null, db) }; req.onerror = function (e) { callback(this.error); e.preventDefault() } }, getLocalSet: function (mount, callback) { var entries = {}; function isRealDir(p) { return p !== "." && p !== ".." } function toAbsolute(root) { return function (p) { return PATH.join2(root, p) } } var check = FS.readdir(mount.mountpoint).filter(isRealDir).map(toAbsolute(mount.mountpoint)); while (check.length) { var path = check.pop(); var stat; try { stat = FS.stat(path) } catch (e) { return callback(e) } if (FS.isDir(stat.mode)) { check.push.apply(check, FS.readdir(path).filter(isRealDir).map(toAbsolute(path))) } entries[path] = { timestamp: stat.mtime } } return callback(null, { type: "local", entries: entries }) }, getRemoteSet: function (mount, callback) { var entries = {}; IDBFS.getDB(mount.mountpoint, function (err, db) { if (err) return callback(err); try { var transaction = db.transaction([IDBFS.DB_STORE_NAME], "readonly"); transaction.onerror = function (e) { callback(this.error); e.preventDefault() }; var store = transaction.objectStore(IDBFS.DB_STORE_NAME); var index = store.index("timestamp"); index.openKeyCursor().onsuccess = function (event) { var cursor = event.target.result; if (!cursor) { return callback(null, { type: "remote", db: db, entries: entries }) } entries[cursor.primaryKey] = { timestamp: cursor.key }; cursor.continue() } } catch (e) { return callback(e) } }) }, loadLocalEntry: function (path, callback) { var stat, node; try { var lookup = FS.lookupPath(path); node = lookup.node; stat = FS.stat(path) } catch (e) { return callback(e) } if (FS.isDir(stat.mode)) { return callback(null, { timestamp: stat.mtime, mode: stat.mode }) } else if (FS.isFile(stat.mode)) { node.contents = MEMFS.getFileDataAsTypedArray(node); return callback(null, { timestamp: stat.mtime, mode: stat.mode, contents: node.contents }) } else { return callback(new Error("node type not supported")) } }, storeLocalEntry: function (path, entry, callback) { try { if (FS.isDir(entry.mode)) { FS.mkdir(path, entry.mode) } else if (FS.isFile(entry.mode)) { FS.writeFile(path, entry.contents, { canOwn: true }) } else { return callback(new Error("node type not supported")) } FS.chmod(path, entry.mode); FS.utime(path, entry.timestamp, entry.timestamp) } catch (e) { return callback(e) } callback(null) }, removeLocalEntry: function (path, callback) { try { var lookup = FS.lookupPath(path); var stat = FS.stat(path); if (FS.isDir(stat.mode)) { FS.rmdir(path) } else if (FS.isFile(stat.mode)) { FS.unlink(path) } } catch (e) { return callback(e) } callback(null) }, loadRemoteEntry: function (store, path, callback) { var req = store.get(path); req.onsuccess = function (event) { callback(null, event.target.result) }; req.onerror = function (e) { callback(this.error); e.preventDefault() } }, storeRemoteEntry: function (store, path, entry, callback) { var req = store.put(entry, path); req.onsuccess = function () { callback(null) }; req.onerror = function (e) { callback(this.error); e.preventDefault() } }, removeRemoteEntry: function (store, path, callback) { var req = store.delete(path); req.onsuccess = function () { callback(null) }; req.onerror = function (e) { callback(this.error); e.preventDefault() } }, reconcile: function (src, dst, callback) { var total = 0; var create = []; Object.keys(src.entries).forEach(function (key) { var e = src.entries[key]; var e2 = dst.entries[key]; if (!e2 || e.timestamp > e2.timestamp) { create.push(key); total++ } }); var remove = []; Object.keys(dst.entries).forEach(function (key) { var e = dst.entries[key]; var e2 = src.entries[key]; if (!e2) { remove.push(key); total++ } }); if (!total) { return callback(null) } var errored = false; var completed = 0; var db = src.type === "remote" ? src.db : dst.db; var transaction = db.transaction([IDBFS.DB_STORE_NAME], "readwrite"); var store = transaction.objectStore(IDBFS.DB_STORE_NAME); function done(err) { if (err) { if (!done.errored) { done.errored = true; return callback(err) } return } if (++completed >= total) { return callback(null) } } transaction.onerror = function (e) { done(this.error); e.preventDefault() }; create.sort().forEach(function (path) { if (dst.type === "local") { IDBFS.loadRemoteEntry(store, path, function (err, entry) { if (err) return done(err); IDBFS.storeLocalEntry(path, entry, done) }) } else { IDBFS.loadLocalEntry(path, function (err, entry) { if (err) return done(err); IDBFS.storeRemoteEntry(store, path, entry, done) }) } }); remove.sort().reverse().forEach(function (path) { if (dst.type === "local") { IDBFS.removeLocalEntry(path, done) } else { IDBFS.removeRemoteEntry(store, path, done) } }) } }; var NODEFS = { isWindows: false, staticInit: function () { NODEFS.isWindows = !!process.platform.match(/^win/); var flags = process["binding"]("constants"); if (flags["fs"]) { flags = flags["fs"] } NODEFS.flagsForNodeMap = { 1024: flags["O_APPEND"], 64: flags["O_CREAT"], 128: flags["O_EXCL"], 0: flags["O_RDONLY"], 2: flags["O_RDWR"], 4096: flags["O_SYNC"], 512: flags["O_TRUNC"], 1: flags["O_WRONLY"] } }, bufferFrom: function (arrayBuffer) { return Buffer.alloc ? Buffer.from(arrayBuffer) : new Buffer(arrayBuffer) }, mount: function (mount) { assert(ENVIRONMENT_IS_NODE); return NODEFS.createNode(null, "/", NODEFS.getMode(mount.opts.root), 0) }, createNode: function (parent, name, mode, dev) { if (!FS.isDir(mode) && !FS.isFile(mode) && !FS.isLink(mode)) { throw new FS.ErrnoError(22) } var node = FS.createNode(parent, name, mode); node.node_ops = NODEFS.node_ops; node.stream_ops = NODEFS.stream_ops; return node }, getMode: function (path) { var stat; try { stat = fs.lstatSync(path); if (NODEFS.isWindows) { stat.mode = stat.mode | (stat.mode & 292) >> 2 } } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } return stat.mode }, realPath: function (node) { var parts = []; while (node.parent !== node) { parts.push(node.name); node = node.parent } parts.push(node.mount.opts.root); parts.reverse(); return PATH.join.apply(null, parts) }, flagsForNode: function (flags) { flags &= ~2097152; flags &= ~2048; flags &= ~32768; flags &= ~524288; var newFlags = 0; for (var k in NODEFS.flagsForNodeMap) { if (flags & k) { newFlags |= NODEFS.flagsForNodeMap[k]; flags ^= k } } if (!flags) { return newFlags } else { throw new FS.ErrnoError(22) } }, node_ops: { getattr: function (node) { var path = NODEFS.realPath(node); var stat; try { stat = fs.lstatSync(path) } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } if (NODEFS.isWindows && !stat.blksize) { stat.blksize = 4096 } if (NODEFS.isWindows && !stat.blocks) { stat.blocks = (stat.size + stat.blksize - 1) / stat.blksize | 0 } return { dev: stat.dev, ino: stat.ino, mode: stat.mode, nlink: stat.nlink, uid: stat.uid, gid: stat.gid, rdev: stat.rdev, size: stat.size, atime: stat.atime, mtime: stat.mtime, ctime: stat.ctime, blksize: stat.blksize, blocks: stat.blocks } }, setattr: function (node, attr) { var path = NODEFS.realPath(node); try { if (attr.mode !== undefined) { fs.chmodSync(path, attr.mode); node.mode = attr.mode } if (attr.timestamp !== undefined) { var date = new Date(attr.timestamp); fs.utimesSync(path, date, date) } if (attr.size !== undefined) { fs.truncateSync(path, attr.size) } } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } }, lookup: function (parent, name) { var path = PATH.join2(NODEFS.realPath(parent), name); var mode = NODEFS.getMode(path); return NODEFS.createNode(parent, name, mode) }, mknod: function (parent, name, mode, dev) { var node = NODEFS.createNode(parent, name, mode, dev); var path = NODEFS.realPath(node); try { if (FS.isDir(node.mode)) { fs.mkdirSync(path, node.mode) } else { fs.writeFileSync(path, "", { mode: node.mode }) } } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } return node }, rename: function (oldNode, newDir, newName) { var oldPath = NODEFS.realPath(oldNode); var newPath = PATH.join2(NODEFS.realPath(newDir), newName); try { fs.renameSync(oldPath, newPath) } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } }, unlink: function (parent, name) { var path = PATH.join2(NODEFS.realPath(parent), name); try { fs.unlinkSync(path) } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } }, rmdir: function (parent, name) { var path = PATH.join2(NODEFS.realPath(parent), name); try { fs.rmdirSync(path) } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } }, readdir: function (node) { var path = NODEFS.realPath(node); try { return fs.readdirSync(path) } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } }, symlink: function (parent, newName, oldPath) { var newPath = PATH.join2(NODEFS.realPath(parent), newName); try { fs.symlinkSync(oldPath, newPath) } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } }, readlink: function (node) { var path = NODEFS.realPath(node); try { path = fs.readlinkSync(path); path = NODEJS_PATH.relative(NODEJS_PATH.resolve(node.mount.opts.root), path); return path } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } } }, stream_ops: { open: function (stream) { var path = NODEFS.realPath(stream.node); try { if (FS.isFile(stream.node.mode)) { stream.nfd = fs.openSync(path, NODEFS.flagsForNode(stream.flags)) } } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } }, close: function (stream) { try { if (FS.isFile(stream.node.mode) && stream.nfd) { fs.closeSync(stream.nfd) } } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(-e.errno) } }, read: function (stream, buffer, offset, length, position) { if (length === 0) return 0; try { return fs.readSync(stream.nfd, NODEFS.bufferFrom(buffer.buffer), offset, length, position) } catch (e) { throw new FS.ErrnoError(-e.errno) } }, write: function (stream, buffer, offset, length, position) { try { return fs.writeSync(stream.nfd, NODEFS.bufferFrom(buffer.buffer), offset, length, position) } catch (e) { throw new FS.ErrnoError(-e.errno) } }, llseek: function (stream, offset, whence) { var position = offset; if (whence === 1) { position += stream.position } else if (whence === 2) { if (FS.isFile(stream.node.mode)) { try { var stat = fs.fstatSync(stream.nfd); position += stat.size } catch (e) { throw new FS.ErrnoError(-e.errno) } } } if (position < 0) { throw new FS.ErrnoError(22) } return position } } }; var WORKERFS = { DIR_MODE: 16895, FILE_MODE: 33279, reader: null, mount: function (mount) { assert(ENVIRONMENT_IS_WORKER); if (!WORKERFS.reader) WORKERFS.reader = new FileReaderSync; var root = WORKERFS.createNode(null, "/", WORKERFS.DIR_MODE, 0); var createdParents = {}; function ensureParent(path) { var parts = path.split("/"); var parent = root; for (var i = 0; i < parts.length - 1; i++) { var curr = parts.slice(0, i + 1).join("/"); if (!createdParents[curr]) { createdParents[curr] = WORKERFS.createNode(parent, parts[i], WORKERFS.DIR_MODE, 0) } parent = createdParents[curr] } return parent } function base(path) { var parts = path.split("/"); return parts[parts.length - 1] } Array.prototype.forEach.call(mount.opts["files"] || [], function (file) { WORKERFS.createNode(ensureParent(file.name), base(file.name), WORKERFS.FILE_MODE, 0, file, file.lastModifiedDate) }); (mount.opts["blobs"] || []).forEach(function (obj) { WORKERFS.createNode(ensureParent(obj["name"]), base(obj["name"]), WORKERFS.FILE_MODE, 0, obj["data"]) }); (mount.opts["packages"] || []).forEach(function (pack) { pack["metadata"].files.forEach(function (file) { var name = file.filename.substr(1); WORKERFS.createNode(ensureParent(name), base(name), WORKERFS.FILE_MODE, 0, pack["blob"].slice(file.start, file.end)) }) }); return root }, createNode: function (parent, name, mode, dev, contents, mtime) { var node = FS.createNode(parent, name, mode); node.mode = mode; node.node_ops = WORKERFS.node_ops; node.stream_ops = WORKERFS.stream_ops; node.timestamp = (mtime || new Date).getTime(); assert(WORKERFS.FILE_MODE !== WORKERFS.DIR_MODE); if (mode === WORKERFS.FILE_MODE) { node.size = contents.size; node.contents = contents } else { node.size = 4096; node.contents = {} } if (parent) { parent.contents[name] = node } return node }, node_ops: { getattr: function (node) { return { dev: 1, ino: undefined, mode: node.mode, nlink: 1, uid: 0, gid: 0, rdev: undefined, size: node.size, atime: new Date(node.timestamp), mtime: new Date(node.timestamp), ctime: new Date(node.timestamp), blksize: 4096, blocks: Math.ceil(node.size / 4096) } }, setattr: function (node, attr) { if (attr.mode !== undefined) { node.mode = attr.mode } if (attr.timestamp !== undefined) { node.timestamp = attr.timestamp } }, lookup: function (parent, name) { throw new FS.ErrnoError(2) }, mknod: function (parent, name, mode, dev) { throw new FS.ErrnoError(1) }, rename: function (oldNode, newDir, newName) { throw new FS.ErrnoError(1) }, unlink: function (parent, name) { throw new FS.ErrnoError(1) }, rmdir: function (parent, name) { throw new FS.ErrnoError(1) }, readdir: function (node) { var entries = [".", ".."]; for (var key in node.contents) { if (!node.contents.hasOwnProperty(key)) { continue } entries.push(key) } return entries }, symlink: function (parent, newName, oldPath) { throw new FS.ErrnoError(1) }, readlink: function (node) { throw new FS.ErrnoError(1) } }, stream_ops: { read: function (stream, buffer, offset, length, position) { if (position >= stream.node.size) return 0; var chunk = stream.node.contents.slice(position, position + length); var ab = WORKERFS.reader.readAsArrayBuffer(chunk); buffer.set(new Uint8Array(ab), offset); return chunk.size }, write: function (stream, buffer, offset, length, position) { throw new FS.ErrnoError(5) }, llseek: function (stream, offset, whence) { var position = offset; if (whence === 1) { position += stream.position } else if (whence === 2) { if (FS.isFile(stream.node.mode)) { position += stream.node.size } } if (position < 0) { throw new FS.ErrnoError(22) } return position } } }; var FS = { root: null, mounts: [], devices: {}, streams: [], nextInode: 1, nameTable: null, currentPath: "/", initialized: false, ignorePermissions: true, trackingDelegate: {}, tracking: { openFlags: { READ: 1, WRITE: 2 } }, ErrnoError: null, genericErrors: {}, filesystems: null, syncFSRequests: 0, handleFSError: function (e) { if (!(e instanceof FS.ErrnoError)) throw e + " : " + stackTrace(); return ___setErrNo(e.errno) }, lookupPath: function (path, opts) { path = PATH_FS.resolve(FS.cwd(), path); opts = opts || {}; if (!path) return { path: "", node: null }; var defaults = { follow_mount: true, recurse_count: 0 }; for (var key in defaults) { if (opts[key] === undefined) { opts[key] = defaults[key] } } if (opts.recurse_count > 8) { throw new FS.ErrnoError(40) } var parts = PATH.normalizeArray(path.split("/").filter(function (p) { return !!p }), false); var current = FS.root; var current_path = "/"; for (var i = 0; i < parts.length; i++) { var islast = i === parts.length - 1; if (islast && opts.parent) { break } current = FS.lookupNode(current, parts[i]); current_path = PATH.join2(current_path, parts[i]); if (FS.isMountpoint(current)) { if (!islast || islast && opts.follow_mount) { current = current.mounted.root } } if (!islast || opts.follow) { var count = 0; while (FS.isLink(current.mode)) { var link = FS.readlink(current_path); current_path = PATH_FS.resolve(PATH.dirname(current_path), link); var lookup = FS.lookupPath(current_path, { recurse_count: opts.recurse_count }); current = lookup.node; if (count++ > 40) { throw new FS.ErrnoError(40) } } } } return { path: current_path, node: current } }, getPath: function (node) { var path; while (true) { if (FS.isRoot(node)) { var mount = node.mount.mountpoint; if (!path) return mount; return mount[mount.length - 1] !== "/" ? mount + "/" + path : mount + path } path = path ? node.name + "/" + path : node.name; node = node.parent } }, hashName: function (parentid, name) { var hash = 0; for (var i = 0; i < name.length; i++) { hash = (hash << 5) - hash + name.charCodeAt(i) | 0 } return (parentid + hash >>> 0) % FS.nameTable.length }, hashAddNode: function (node) { var hash = FS.hashName(node.parent.id, node.name); node.name_next = FS.nameTable[hash]; FS.nameTable[hash] = node }, hashRemoveNode: function (node) { var hash = FS.hashName(node.parent.id, node.name); if (FS.nameTable[hash] === node) { FS.nameTable[hash] = node.name_next } else { var current = FS.nameTable[hash]; while (current) { if (current.name_next === node) { current.name_next = node.name_next; break } current = current.name_next } } }, lookupNode: function (parent, name) { var err = FS.mayLookup(parent); if (err) { throw new FS.ErrnoError(err, parent) } var hash = FS.hashName(parent.id, name); for (var node = FS.nameTable[hash]; node; node = node.name_next) { var nodeName = node.name; if (node.parent.id === parent.id && nodeName === name) { return node } } return FS.lookup(parent, name) }, createNode: function (parent, name, mode, rdev) { if (!FS.FSNode) { FS.FSNode = function (parent, name, mode, rdev) { if (!parent) { parent = this } this.parent = parent; this.mount = parent.mount; this.mounted = null; this.id = FS.nextInode++; this.name = name; this.mode = mode; this.node_ops = {}; this.stream_ops = {}; this.rdev = rdev }; FS.FSNode.prototype = {}; var readMode = 292 | 73; var writeMode = 146; Object.defineProperties(FS.FSNode.prototype, { read: { get: function () { return (this.mode & readMode) === readMode }, set: function (val) { val ? this.mode |= readMode : this.mode &= ~readMode } }, write: { get: function () { return (this.mode & writeMode) === writeMode }, set: function (val) { val ? this.mode |= writeMode : this.mode &= ~writeMode } }, isFolder: { get: function () { return FS.isDir(this.mode) } }, isDevice: { get: function () { return FS.isChrdev(this.mode) } } }) } var node = new FS.FSNode(parent, name, mode, rdev); FS.hashAddNode(node); return node }, destroyNode: function (node) { FS.hashRemoveNode(node) }, isRoot: function (node) { return node === node.parent }, isMountpoint: function (node) { return !!node.mounted }, isFile: function (mode) { return (mode & 61440) === 32768 }, isDir: function (mode) { return (mode & 61440) === 16384 }, isLink: function (mode) { return (mode & 61440) === 40960 }, isChrdev: function (mode) { return (mode & 61440) === 8192 }, isBlkdev: function (mode) { return (mode & 61440) === 24576 }, isFIFO: function (mode) { return (mode & 61440) === 4096 }, isSocket: function (mode) { return (mode & 49152) === 49152 }, flagModes: { "r": 0, "rs": 1052672, "r+": 2, "w": 577, "wx": 705, "xw": 705, "w+": 578, "wx+": 706, "xw+": 706, "a": 1089, "ax": 1217, "xa": 1217, "a+": 1090, "ax+": 1218, "xa+": 1218 }, modeStringToFlags: function (str) { var flags = FS.flagModes[str]; if (typeof flags === "undefined") { throw new Error("Unknown file open mode: " + str) } return flags }, flagsToPermissionString: function (flag) { var perms = ["r", "w", "rw"][flag & 3]; if (flag & 512) { perms += "w" } return perms }, nodePermissions: function (node, perms) { if (FS.ignorePermissions) { return 0 } if (perms.indexOf("r") !== -1 && !(node.mode & 292)) { return 13 } else if (perms.indexOf("w") !== -1 && !(node.mode & 146)) { return 13 } else if (perms.indexOf("x") !== -1 && !(node.mode & 73)) { return 13 } return 0 }, mayLookup: function (dir) { var err = FS.nodePermissions(dir, "x"); if (err) return err; if (!dir.node_ops.lookup) return 13; return 0 }, mayCreate: function (dir, name) { try { var node = FS.lookupNode(dir, name); return 17 } catch (e) { } return FS.nodePermissions(dir, "wx") }, mayDelete: function (dir, name, isdir) { var node; try { node = FS.lookupNode(dir, name) } catch (e) { return e.errno } var err = FS.nodePermissions(dir, "wx"); if (err) { return err } if (isdir) { if (!FS.isDir(node.mode)) { return 20 } if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) { return 16 } } else { if (FS.isDir(node.mode)) { return 21 } } return 0 }, mayOpen: function (node, flags) { if (!node) { return 2 } if (FS.isLink(node.mode)) { return 40 } else if (FS.isDir(node.mode)) { if (FS.flagsToPermissionString(flags) !== "r" || flags & 512) { return 21 } } return FS.nodePermissions(node, FS.flagsToPermissionString(flags)) }, MAX_OPEN_FDS: 4096, nextfd: function (fd_start, fd_end) { fd_start = fd_start || 0; fd_end = fd_end || FS.MAX_OPEN_FDS; for (var fd = fd_start; fd <= fd_end; fd++) { if (!FS.streams[fd]) { return fd } } throw new FS.ErrnoError(24) }, getStream: function (fd) { return FS.streams[fd] }, createStream: function (stream, fd_start, fd_end) { if (!FS.FSStream) { FS.FSStream = function () { }; FS.FSStream.prototype = {}; Object.defineProperties(FS.FSStream.prototype, { object: { get: function () { return this.node }, set: function (val) { this.node = val } }, isRead: { get: function () { return (this.flags & 2097155) !== 1 } }, isWrite: { get: function () { return (this.flags & 2097155) !== 0 } }, isAppend: { get: function () { return this.flags & 1024 } } }) } var newStream = new FS.FSStream; for (var p in stream) { newStream[p] = stream[p] } stream = newStream; var fd = FS.nextfd(fd_start, fd_end); stream.fd = fd; FS.streams[fd] = stream; return stream }, closeStream: function (fd) { FS.streams[fd] = null }, chrdev_stream_ops: { open: function (stream) { var device = FS.getDevice(stream.node.rdev); stream.stream_ops = device.stream_ops; if (stream.stream_ops.open) { stream.stream_ops.open(stream) } }, llseek: function () { throw new FS.ErrnoError(29) } }, major: function (dev) { return dev >> 8 }, minor: function (dev) { return dev & 255 }, makedev: function (ma, mi) { return ma << 8 | mi }, registerDevice: function (dev, ops) { FS.devices[dev] = { stream_ops: ops } }, getDevice: function (dev) { return FS.devices[dev] }, getMounts: function (mount) { var mounts = []; var check = [mount]; while (check.length) { var m = check.pop(); mounts.push(m); check.push.apply(check, m.mounts) } return mounts }, syncfs: function (populate, callback) { if (typeof populate === "function") { callback = populate; populate = false } FS.syncFSRequests++; if (FS.syncFSRequests > 1) { console.log("warning: " + FS.syncFSRequests + " FS.syncfs operations in flight at once, probably just doing extra work") } var mounts = FS.getMounts(FS.root.mount); var completed = 0; function doCallback(err) { FS.syncFSRequests--; return callback(err) } function done(err) { if (err) { if (!done.errored) { done.errored = true; return doCallback(err) } return } if (++completed >= mounts.length) { doCallback(null) } } mounts.forEach(function (mount) { if (!mount.type.syncfs) { return done(null) } mount.type.syncfs(mount, populate, done) }) }, mount: function (type, opts, mountpoint) { var root = mountpoint === "/"; var pseudo = !mountpoint; var node; if (root && FS.root) { throw new FS.ErrnoError(16) } else if (!root && !pseudo) { var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); mountpoint = lookup.path; node = lookup.node; if (FS.isMountpoint(node)) { throw new FS.ErrnoError(16) } if (!FS.isDir(node.mode)) { throw new FS.ErrnoError(20) } } var mount = { type: type, opts: opts, mountpoint: mountpoint, mounts: [] }; var mountRoot = type.mount(mount); mountRoot.mount = mount; mount.root = mountRoot; if (root) { FS.root = mountRoot } else if (node) { node.mounted = mount; if (node.mount) { node.mount.mounts.push(mount) } } return mountRoot }, unmount: function (mountpoint) { var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); if (!FS.isMountpoint(lookup.node)) { throw new FS.ErrnoError(22) } var node = lookup.node; var mount = node.mounted; var mounts = FS.getMounts(mount); Object.keys(FS.nameTable).forEach(function (hash) { var current = FS.nameTable[hash]; while (current) { var next = current.name_next; if (mounts.indexOf(current.mount) !== -1) { FS.destroyNode(current) } current = next } }); node.mounted = null; var idx = node.mount.mounts.indexOf(mount); node.mount.mounts.splice(idx, 1) }, lookup: function (parent, name) { return parent.node_ops.lookup(parent, name) }, mknod: function (path, mode, dev) { var lookup = FS.lookupPath(path, { parent: true }); var parent = lookup.node; var name = PATH.basename(path); if (!name || name === "." || name === "..") { throw new FS.ErrnoError(22) } var err = FS.mayCreate(parent, name); if (err) { throw new FS.ErrnoError(err) } if (!parent.node_ops.mknod) { throw new FS.ErrnoError(1) } return parent.node_ops.mknod(parent, name, mode, dev) }, create: function (path, mode) { mode = mode !== undefined ? mode : 438; mode &= 4095; mode |= 32768; return FS.mknod(path, mode, 0) }, mkdir: function (path, mode) { mode = mode !== undefined ? mode : 511; mode &= 511 | 512; mode |= 16384; return FS.mknod(path, mode, 0) }, mkdirTree: function (path, mode) { var dirs = path.split("/"); var d = ""; for (var i = 0; i < dirs.length; ++i) { if (!dirs[i]) continue; d += "/" + dirs[i]; try { FS.mkdir(d, mode) } catch (e) { if (e.errno != 17) throw e } } }, mkdev: function (path, mode, dev) { if (typeof dev === "undefined") { dev = mode; mode = 438 } mode |= 8192; return FS.mknod(path, mode, dev) }, symlink: function (oldpath, newpath) { if (!PATH_FS.resolve(oldpath)) { throw new FS.ErrnoError(2) } var lookup = FS.lookupPath(newpath, { parent: true }); var parent = lookup.node; if (!parent) { throw new FS.ErrnoError(2) } var newname = PATH.basename(newpath); var err = FS.mayCreate(parent, newname); if (err) { throw new FS.ErrnoError(err) } if (!parent.node_ops.symlink) { throw new FS.ErrnoError(1) } return parent.node_ops.symlink(parent, newname, oldpath) }, rename: function (old_path, new_path) { var old_dirname = PATH.dirname(old_path); var new_dirname = PATH.dirname(new_path); var old_name = PATH.basename(old_path); var new_name = PATH.basename(new_path); var lookup, old_dir, new_dir; try { lookup = FS.lookupPath(old_path, { parent: true }); old_dir = lookup.node; lookup = FS.lookupPath(new_path, { parent: true }); new_dir = lookup.node } catch (e) { throw new FS.ErrnoError(16) } if (!old_dir || !new_dir) throw new FS.ErrnoError(2); if (old_dir.mount !== new_dir.mount) { throw new FS.ErrnoError(18) } var old_node = FS.lookupNode(old_dir, old_name); var relative = PATH_FS.relative(old_path, new_dirname); if (relative.charAt(0) !== ".") { throw new FS.ErrnoError(22) } relative = PATH_FS.relative(new_path, old_dirname); if (relative.charAt(0) !== ".") { throw new FS.ErrnoError(39) } var new_node; try { new_node = FS.lookupNode(new_dir, new_name) } catch (e) { } if (old_node === new_node) { return } var isdir = FS.isDir(old_node.mode); var err = FS.mayDelete(old_dir, old_name, isdir); if (err) { throw new FS.ErrnoError(err) } err = new_node ? FS.mayDelete(new_dir, new_name, isdir) : FS.mayCreate(new_dir, new_name); if (err) { throw new FS.ErrnoError(err) } if (!old_dir.node_ops.rename) { throw new FS.ErrnoError(1) } if (FS.isMountpoint(old_node) || new_node && FS.isMountpoint(new_node)) { throw new FS.ErrnoError(16) } if (new_dir !== old_dir) { err = FS.nodePermissions(old_dir, "w"); if (err) { throw new FS.ErrnoError(err) } } try { if (FS.trackingDelegate["willMovePath"]) { FS.trackingDelegate["willMovePath"](old_path, new_path) } } catch (e) { console.log("FS.trackingDelegate['willMovePath']('" + old_path + "', '" + new_path + "') threw an exception: " + e.message) } FS.hashRemoveNode(old_node); try { old_dir.node_ops.rename(old_node, new_dir, new_name) } catch (e) { throw e } finally { FS.hashAddNode(old_node) } try { if (FS.trackingDelegate["onMovePath"]) FS.trackingDelegate["onMovePath"](old_path, new_path) } catch (e) { console.log("FS.trackingDelegate['onMovePath']('" + old_path + "', '" + new_path + "') threw an exception: " + e.message) } }, rmdir: function (path) { var lookup = FS.lookupPath(path, { parent: true }); var parent = lookup.node; var name = PATH.basename(path); var node = FS.lookupNode(parent, name); var err = FS.mayDelete(parent, name, true); if (err) { throw new FS.ErrnoError(err) } if (!parent.node_ops.rmdir) { throw new FS.ErrnoError(1) } if (FS.isMountpoint(node)) { throw new FS.ErrnoError(16) } try { if (FS.trackingDelegate["willDeletePath"]) { FS.trackingDelegate["willDeletePath"](path) } } catch (e) { console.log("FS.trackingDelegate['willDeletePath']('" + path + "') threw an exception: " + e.message) } parent.node_ops.rmdir(parent, name); FS.destroyNode(node); try { if (FS.trackingDelegate["onDeletePath"]) FS.trackingDelegate["onDeletePath"](path) } catch (e) { console.log("FS.trackingDelegate['onDeletePath']('" + path + "') threw an exception: " + e.message) } }, readdir: function (path) { var lookup = FS.lookupPath(path, { follow: true }); var node = lookup.node; if (!node.node_ops.readdir) { throw new FS.ErrnoError(20) } return node.node_ops.readdir(node) }, unlink: function (path) { var lookup = FS.lookupPath(path, { parent: true }); var parent = lookup.node; var name = PATH.basename(path); var node = FS.lookupNode(parent, name); var err = FS.mayDelete(parent, name, false); if (err) { throw new FS.ErrnoError(err) } if (!parent.node_ops.unlink) { throw new FS.ErrnoError(1) } if (FS.isMountpoint(node)) { throw new FS.ErrnoError(16) } try { if (FS.trackingDelegate["willDeletePath"]) { FS.trackingDelegate["willDeletePath"](path) } } catch (e) { console.log("FS.trackingDelegate['willDeletePath']('" + path + "') threw an exception: " + e.message) } parent.node_ops.unlink(parent, name); FS.destroyNode(node); try { if (FS.trackingDelegate["onDeletePath"]) FS.trackingDelegate["onDeletePath"](path) } catch (e) { console.log("FS.trackingDelegate['onDeletePath']('" + path + "') threw an exception: " + e.message) } }, readlink: function (path) { var lookup = FS.lookupPath(path); var link = lookup.node; if (!link) { throw new FS.ErrnoError(2) } if (!link.node_ops.readlink) { throw new FS.ErrnoError(22) } return PATH_FS.resolve(FS.getPath(link.parent), link.node_ops.readlink(link)) }, stat: function (path, dontFollow) { var lookup = FS.lookupPath(path, { follow: !dontFollow }); var node = lookup.node; if (!node) { throw new FS.ErrnoError(2) } if (!node.node_ops.getattr) { throw new FS.ErrnoError(1) } return node.node_ops.getattr(node) }, lstat: function (path) { return FS.stat(path, true) }, chmod: function (path, mode, dontFollow) { var node; if (typeof path === "string") { var lookup = FS.lookupPath(path, { follow: !dontFollow }); node = lookup.node } else { node = path } if (!node.node_ops.setattr) { throw new FS.ErrnoError(1) } node.node_ops.setattr(node, { mode: mode & 4095 | node.mode & ~4095, timestamp: Date.now() }) }, lchmod: function (path, mode) { FS.chmod(path, mode, true) }, fchmod: function (fd, mode) { var stream = FS.getStream(fd); if (!stream) { throw new FS.ErrnoError(9) } FS.chmod(stream.node, mode) }, chown: function (path, uid, gid, dontFollow) { var node; if (typeof path === "string") { var lookup = FS.lookupPath(path, { follow: !dontFollow }); node = lookup.node } else { node = path } if (!node.node_ops.setattr) { throw new FS.ErrnoError(1) } node.node_ops.setattr(node, { timestamp: Date.now() }) }, lchown: function (path, uid, gid) { FS.chown(path, uid, gid, true) }, fchown: function (fd, uid, gid) { var stream = FS.getStream(fd); if (!stream) { throw new FS.ErrnoError(9) } FS.chown(stream.node, uid, gid) }, truncate: function (path, len) { if (len < 0) { throw new FS.ErrnoError(22) } var node; if (typeof path === "string") { var lookup = FS.lookupPath(path, { follow: true }); node = lookup.node } else { node = path } if (!node.node_ops.setattr) { throw new FS.ErrnoError(1) } if (FS.isDir(node.mode)) { throw new FS.ErrnoError(21) } if (!FS.isFile(node.mode)) { throw new FS.ErrnoError(22) } var err = FS.nodePermissions(node, "w"); if (err) { throw new FS.ErrnoError(err) } node.node_ops.setattr(node, { size: len, timestamp: Date.now() }) }, ftruncate: function (fd, len) { var stream = FS.getStream(fd); if (!stream) { throw new FS.ErrnoError(9) } if ((stream.flags & 2097155) === 0) { throw new FS.ErrnoError(22) } FS.truncate(stream.node, len) }, utime: function (path, atime, mtime) { var lookup = FS.lookupPath(path, { follow: true }); var node = lookup.node; node.node_ops.setattr(node, { timestamp: Math.max(atime, mtime) }) }, open: function (path, flags, mode, fd_start, fd_end) { if (path === "") { throw new FS.ErrnoError(2) } flags = typeof flags === "string" ? FS.modeStringToFlags(flags) : flags; mode = typeof mode === "undefined" ? 438 : mode; if (flags & 64) { mode = mode & 4095 | 32768 } else { mode = 0 } var node; if (typeof path === "object") { node = path } else { path = PATH.normalize(path); try { var lookup = FS.lookupPath(path, { follow: !(flags & 131072) }); node = lookup.node } catch (e) { } } var created = false; if (flags & 64) { if (node) { if (flags & 128) { throw new FS.ErrnoError(17) } } else { node = FS.mknod(path, mode, 0); created = true } } if (!node) { throw new FS.ErrnoError(2) } if (FS.isChrdev(node.mode)) { flags &= ~512 } if (flags & 65536 && !FS.isDir(node.mode)) { throw new FS.ErrnoError(20) } if (!created) { var err = FS.mayOpen(node, flags); if (err) { throw new FS.ErrnoError(err) } } if (flags & 512) { FS.truncate(node, 0) } flags &= ~(128 | 512); var stream = FS.createStream({ node: node, path: FS.getPath(node), flags: flags, seekable: true, position: 0, stream_ops: node.stream_ops, ungotten: [], error: false }, fd_start, fd_end); if (stream.stream_ops.open) { stream.stream_ops.open(stream) } if (Module["logReadFiles"] && !(flags & 1)) { if (!FS.readFiles) FS.readFiles = {}; if (!(path in FS.readFiles)) { FS.readFiles[path] = 1; console.log("FS.trackingDelegate error on read file: " + path) } } try { if (FS.trackingDelegate["onOpenFile"]) { var trackingFlags = 0; if ((flags & 2097155) !== 1) { trackingFlags |= FS.tracking.openFlags.READ } if ((flags & 2097155) !== 0) { trackingFlags |= FS.tracking.openFlags.WRITE } FS.trackingDelegate["onOpenFile"](path, trackingFlags) } } catch (e) { console.log("FS.trackingDelegate['onOpenFile']('" + path + "', flags) threw an exception: " + e.message) } return stream }, close: function (stream) { if (FS.isClosed(stream)) { throw new FS.ErrnoError(9) } if (stream.getdents) stream.getdents = null; try { if (stream.stream_ops.close) { stream.stream_ops.close(stream) } } catch (e) { throw e } finally { FS.closeStream(stream.fd) } stream.fd = null }, isClosed: function (stream) { return stream.fd === null }, llseek: function (stream, offset, whence) { if (FS.isClosed(stream)) { throw new FS.ErrnoError(9) } if (!stream.seekable || !stream.stream_ops.llseek) { throw new FS.ErrnoError(29) } if (whence != 0 && whence != 1 && whence != 2) { throw new FS.ErrnoError(22) } stream.position = stream.stream_ops.llseek(stream, offset, whence); stream.ungotten = []; return stream.position }, read: function (stream, buffer, offset, length, position) { if (length < 0 || position < 0) { throw new FS.ErrnoError(22) } if (FS.isClosed(stream)) { throw new FS.ErrnoError(9) } if ((stream.flags & 2097155) === 1) { throw new FS.ErrnoError(9) } if (FS.isDir(stream.node.mode)) { throw new FS.ErrnoError(21) } if (!stream.stream_ops.read) { throw new FS.ErrnoError(22) } var seeking = typeof position !== "undefined"; if (!seeking) { position = stream.position } else if (!stream.seekable) { throw new FS.ErrnoError(29) } var bytesRead = stream.stream_ops.read(stream, buffer, offset, length, position); if (!seeking) stream.position += bytesRead; return bytesRead }, write: function (stream, buffer, offset, length, position, canOwn) { if (length < 0 || position < 0) { throw new FS.ErrnoError(22) } if (FS.isClosed(stream)) { throw new FS.ErrnoError(9) } if ((stream.flags & 2097155) === 0) { throw new FS.ErrnoError(9) } if (FS.isDir(stream.node.mode)) { throw new FS.ErrnoError(21) } if (!stream.stream_ops.write) { throw new FS.ErrnoError(22) } if (stream.flags & 1024) { FS.llseek(stream, 0, 2) } var seeking = typeof position !== "undefined"; if (!seeking) { position = stream.position } else if (!stream.seekable) { throw new FS.ErrnoError(29) } var bytesWritten = stream.stream_ops.write(stream, buffer, offset, length, position, canOwn); if (!seeking) stream.position += bytesWritten; try { if (stream.path && FS.trackingDelegate["onWriteToFile"]) FS.trackingDelegate["onWriteToFile"](stream.path) } catch (e) { console.log("FS.trackingDelegate['onWriteToFile']('" + stream.path + "') threw an exception: " + e.message) } return bytesWritten }, allocate: function (stream, offset, length) { if (FS.isClosed(stream)) { throw new FS.ErrnoError(9) } if (offset < 0 || length <= 0) { throw new FS.ErrnoError(22) } if ((stream.flags & 2097155) === 0) { throw new FS.ErrnoError(9) } if (!FS.isFile(stream.node.mode) && !FS.isDir(stream.node.mode)) { throw new FS.ErrnoError(19) } if (!stream.stream_ops.allocate) { throw new FS.ErrnoError(95) } stream.stream_ops.allocate(stream, offset, length) }, mmap: function (stream, buffer, offset, length, position, prot, flags) { if ((stream.flags & 2097155) === 1) { throw new FS.ErrnoError(13) } if (!stream.stream_ops.mmap) { throw new FS.ErrnoError(19) } return stream.stream_ops.mmap(stream, buffer, offset, length, position, prot, flags) }, msync: function (stream, buffer, offset, length, mmapFlags) { if (!stream || !stream.stream_ops.msync) { return 0 } return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags) }, munmap: function (stream) { return 0 }, ioctl: function (stream, cmd, arg) { if (!stream.stream_ops.ioctl) { throw new FS.ErrnoError(25) } return stream.stream_ops.ioctl(stream, cmd, arg) }, readFile: function (path, opts) { opts = opts || {}; opts.flags = opts.flags || "r"; opts.encoding = opts.encoding || "binary"; if (opts.encoding !== "utf8" && opts.encoding !== "binary") { throw new Error('Invalid encoding type "' + opts.encoding + '"') } var ret; var stream = FS.open(path, opts.flags); var stat = FS.stat(path); var length = stat.size; var buf = new Uint8Array(length); FS.read(stream, buf, 0, length, 0); if (opts.encoding === "utf8") { ret = UTF8ArrayToString(buf, 0) } else if (opts.encoding === "binary") { ret = buf } FS.close(stream); return ret }, writeFile: function (path, data, opts) { opts = opts || {}; opts.flags = opts.flags || "w"; var stream = FS.open(path, opts.flags, opts.mode); if (typeof data === "string") { var buf = new Uint8Array(lengthBytesUTF8(data) + 1); var actualNumBytes = stringToUTF8Array(data, buf, 0, buf.length); FS.write(stream, buf, 0, actualNumBytes, undefined, opts.canOwn) } else if (ArrayBuffer.isView(data)) { FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn) } else { throw new Error("Unsupported data type") } FS.close(stream) }, cwd: function () { return FS.currentPath }, chdir: function (path) { var lookup = FS.lookupPath(path, { follow: true }); if (lookup.node === null) { throw new FS.ErrnoError(2) } if (!FS.isDir(lookup.node.mode)) { throw new FS.ErrnoError(20) } var err = FS.nodePermissions(lookup.node, "x"); if (err) { throw new FS.ErrnoError(err) } FS.currentPath = lookup.path }, createDefaultDirectories: function () { FS.mkdir("/tmp"); FS.mkdir("/home"); FS.mkdir("/home/web_user") }, createDefaultDevices: function () { FS.mkdir("/dev"); FS.registerDevice(FS.makedev(1, 3), { read: function () { return 0 }, write: function (stream, buffer, offset, length, pos) { return length } }); FS.mkdev("/dev/null", FS.makedev(1, 3)); TTY.register(FS.makedev(5, 0), TTY.default_tty_ops); TTY.register(FS.makedev(6, 0), TTY.default_tty1_ops); FS.mkdev("/dev/tty", FS.makedev(5, 0)); FS.mkdev("/dev/tty1", FS.makedev(6, 0)); var random_device; if (typeof crypto === "object" && typeof crypto["getRandomValues"] === "function") { var randomBuffer = new Uint8Array(1); random_device = function () { crypto.getRandomValues(randomBuffer); return randomBuffer[0] } } else if (ENVIRONMENT_IS_NODE) { try { var crypto_module = require("crypto"); random_device = function () { return crypto_module["randomBytes"](1)[0] } } catch (e) { } } else { } if (!random_device) { random_device = function () { abort("random_device") } } FS.createDevice("/dev", "random", random_device); FS.createDevice("/dev", "urandom", random_device); FS.mkdir("/dev/shm"); FS.mkdir("/dev/shm/tmp") }, createSpecialDirectories: function () { FS.mkdir("/proc"); FS.mkdir("/proc/self"); FS.mkdir("/proc/self/fd"); FS.mount({ mount: function () { var node = FS.createNode("/proc/self", "fd", 16384 | 511, 73); node.node_ops = { lookup: function (parent, name) { var fd = +name; var stream = FS.getStream(fd); if (!stream) throw new FS.ErrnoError(9); var ret = { parent: null, mount: { mountpoint: "fake" }, node_ops: { readlink: function () { return stream.path } } }; ret.parent = ret; return ret } }; return node } }, {}, "/proc/self/fd") }, createStandardStreams: function () { if (Module["stdin"]) { FS.createDevice("/dev", "stdin", Module["stdin"]) } else { FS.symlink("/dev/tty", "/dev/stdin") } if (Module["stdout"]) { FS.createDevice("/dev", "stdout", null, Module["stdout"]) } else { FS.symlink("/dev/tty", "/dev/stdout") } if (Module["stderr"]) { FS.createDevice("/dev", "stderr", null, Module["stderr"]) } else { FS.symlink("/dev/tty1", "/dev/stderr") } var stdin = FS.open("/dev/stdin", "r"); var stdout = FS.open("/dev/stdout", "w"); var stderr = FS.open("/dev/stderr", "w") }, ensureErrnoError: function () { if (FS.ErrnoError) return; FS.ErrnoError = function ErrnoError(errno, node) { this.node = node; this.setErrno = function (errno) { this.errno = errno }; this.setErrno(errno); this.message = "FS error"; if (this.stack) Object.defineProperty(this, "stack", { value: (new Error).stack, writable: true }) }; FS.ErrnoError.prototype = new Error; FS.ErrnoError.prototype.constructor = FS.ErrnoError;[2].forEach(function (code) { FS.genericErrors[code] = new FS.ErrnoError(code); FS.genericErrors[code].stack = "<generic error, no stack>" }) }, staticInit: function () { FS.ensureErrnoError(); FS.nameTable = new Array(4096); FS.mount(MEMFS, {}, "/"); FS.createDefaultDirectories(); FS.createDefaultDevices(); FS.createSpecialDirectories(); FS.filesystems = { "MEMFS": MEMFS, "IDBFS": IDBFS, "NODEFS": NODEFS, "WORKERFS": WORKERFS } }, init: function (input, output, error) { FS.init.initialized = true; FS.ensureErrnoError(); Module["stdin"] = input || Module["stdin"]; Module["stdout"] = output || Module["stdout"]; Module["stderr"] = error || Module["stderr"]; FS.createStandardStreams() }, quit: function () { FS.init.initialized = false; var fflush = Module["_fflush"]; if (fflush) fflush(0); for (var i = 0; i < FS.streams.length; i++) { var stream = FS.streams[i]; if (!stream) { continue } FS.close(stream) } }, getMode: function (canRead, canWrite) { var mode = 0; if (canRead) mode |= 292 | 73; if (canWrite) mode |= 146; return mode }, joinPath: function (parts, forceRelative) { var path = PATH.join.apply(null, parts); if (forceRelative && path[0] == "/") path = path.substr(1); return path }, absolutePath: function (relative, base) { return PATH_FS.resolve(base, relative) }, standardizePath: function (path) { return PATH.normalize(path) }, findObject: function (path, dontResolveLastLink) { var ret = FS.analyzePath(path, dontResolveLastLink); if (ret.exists) { return ret.object } else { ___setErrNo(ret.error); return null } }, analyzePath: function (path, dontResolveLastLink) { try { var lookup = FS.lookupPath(path, { follow: !dontResolveLastLink }); path = lookup.path } catch (e) { } var ret = { isRoot: false, exists: false, error: 0, name: null, path: null, object: null, parentExists: false, parentPath: null, parentObject: null }; try { var lookup = FS.lookupPath(path, { parent: true }); ret.parentExists = true; ret.parentPath = lookup.path; ret.parentObject = lookup.node; ret.name = PATH.basename(path); lookup = FS.lookupPath(path, { follow: !dontResolveLastLink }); ret.exists = true; ret.path = lookup.path; ret.object = lookup.node; ret.name = lookup.node.name; ret.isRoot = lookup.path === "/" } catch (e) { ret.error = e.errno } return ret }, createFolder: function (parent, name, canRead, canWrite) { var path = PATH.join2(typeof parent === "string" ? parent : FS.getPath(parent), name); var mode = FS.getMode(canRead, canWrite); return FS.mkdir(path, mode) }, createPath: function (parent, path, canRead, canWrite) { parent = typeof parent === "string" ? parent : FS.getPath(parent); var parts = path.split("/").reverse(); while (parts.length) { var part = parts.pop(); if (!part) continue; var current = PATH.join2(parent, part); try { FS.mkdir(current) } catch (e) { } parent = current } return current }, createFile: function (parent, name, properties, canRead, canWrite) { var path = PATH.join2(typeof parent === "string" ? parent : FS.getPath(parent), name); var mode = FS.getMode(canRead, canWrite); return FS.create(path, mode) }, createDataFile: function (parent, name, data, canRead, canWrite, canOwn) { var path = name ? PATH.join2(typeof parent === "string" ? parent : FS.getPath(parent), name) : parent; var mode = FS.getMode(canRead, canWrite); var node = FS.create(path, mode); if (data) { if (typeof data === "string") { var arr = new Array(data.length); for (var i = 0, len = data.length; i < len; ++i)arr[i] = data.charCodeAt(i); data = arr } FS.chmod(node, mode | 146); var stream = FS.open(node, "w"); FS.write(stream, data, 0, data.length, 0, canOwn); FS.close(stream); FS.chmod(node, mode) } return node }, createDevice: function (parent, name, input, output) { var path = PATH.join2(typeof parent === "string" ? parent : FS.getPath(parent), name); var mode = FS.getMode(!!input, !!output); if (!FS.createDevice.major) FS.createDevice.major = 64; var dev = FS.makedev(FS.createDevice.major++, 0); FS.registerDevice(dev, { open: function (stream) { stream.seekable = false }, close: function (stream) { if (output && output.buffer && output.buffer.length) { output(10) } }, read: function (stream, buffer, offset, length, pos) { var bytesRead = 0; for (var i = 0; i < length; i++) { var result; try { result = input() } catch (e) { throw new FS.ErrnoError(5) } if (result === undefined && bytesRead === 0) { throw new FS.ErrnoError(11) } if (result === null || result === undefined) break; bytesRead++; buffer[offset + i] = result } if (bytesRead) { stream.node.timestamp = Date.now() } return bytesRead }, write: function (stream, buffer, offset, length, pos) { for (var i = 0; i < length; i++) { try { output(buffer[offset + i]) } catch (e) { throw new FS.ErrnoError(5) } } if (length) { stream.node.timestamp = Date.now() } return i } }); return FS.mkdev(path, mode, dev) }, createLink: function (parent, name, target, canRead, canWrite) { var path = PATH.join2(typeof parent === "string" ? parent : FS.getPath(parent), name); return FS.symlink(target, path) }, forceLoadFile: function (obj) { if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; var success = true; if (typeof XMLHttpRequest !== "undefined") { throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.") } else if (Module["read"]) { try { obj.contents = intArrayFromString(Module["read"](obj.url), true); obj.usedBytes = obj.contents.length } catch (e) { success = false } } else { throw new Error("Cannot load without read() or XMLHttpRequest.") } if (!success) ___setErrNo(5); return success }, createLazyFile: function (parent, name, url, canRead, canWrite) { function LazyUint8Array() { this.lengthKnown = false; this.chunks = [] } LazyUint8Array.prototype.get = function LazyUint8Array_get(idx) { if (idx > this.length - 1 || idx < 0) { return undefined } var chunkOffset = idx % this.chunkSize; var chunkNum = idx / this.chunkSize | 0; return this.getter(chunkNum)[chunkOffset] }; LazyUint8Array.prototype.setDataGetter = function LazyUint8Array_setDataGetter(getter) { this.getter = getter }; LazyUint8Array.prototype.cacheLength = function LazyUint8Array_cacheLength() { var xhr = new XMLHttpRequest; xhr.open("HEAD", url, false); xhr.send(null); if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); var datalength = Number(xhr.getResponseHeader("Content-length")); var header; var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; var chunkSize = 1024 * 1024; if (!hasByteServing) chunkSize = datalength; var doXHR = function (from, to) { if (from > to) throw new Error("invalid range (" + from + ", " + to + ") or no bytes requested!"); if (to > datalength - 1) throw new Error("only " + datalength + " bytes available! programmer error!"); var xhr = new XMLHttpRequest; xhr.open("GET", url, false); if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); if (typeof Uint8Array != "undefined") xhr.responseType = "arraybuffer"; if (xhr.overrideMimeType) { xhr.overrideMimeType("text/plain; charset=x-user-defined") } xhr.send(null); if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); if (xhr.response !== undefined) { return new Uint8Array(xhr.response || []) } else { return intArrayFromString(xhr.responseText || "", true) } }; var lazyArray = this; lazyArray.setDataGetter(function (chunkNum) { var start = chunkNum * chunkSize; var end = (chunkNum + 1) * chunkSize - 1; end = Math.min(end, datalength - 1); if (typeof lazyArray.chunks[chunkNum] === "undefined") { lazyArray.chunks[chunkNum] = doXHR(start, end) } if (typeof lazyArray.chunks[chunkNum] === "undefined") throw new Error("doXHR failed!"); return lazyArray.chunks[chunkNum] }); if (usesGzip || !datalength) { chunkSize = datalength = 1; datalength = this.getter(0).length; chunkSize = datalength; console.log("LazyFiles on gzip forces download of the whole file when length is accessed") } this._length = datalength; this._chunkSize = chunkSize; this.lengthKnown = true }; if (typeof XMLHttpRequest !== "undefined") { if (!ENVIRONMENT_IS_WORKER) throw "Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc"; var lazyArray = new LazyUint8Array; Object.defineProperties(lazyArray, { length: { get: function () { if (!this.lengthKnown) { this.cacheLength() } return this._length } }, chunkSize: { get: function () { if (!this.lengthKnown) { this.cacheLength() } return this._chunkSize } } }); var properties = { isDevice: false, contents: lazyArray } } else { var properties = { isDevice: false, url: url } } var node = FS.createFile(parent, name, properties, canRead, canWrite); if (properties.contents) { node.contents = properties.contents } else if (properties.url) { node.contents = null; node.url = properties.url } Object.defineProperties(node, { usedBytes: { get: function () { return this.contents.length } } }); var stream_ops = {}; var keys = Object.keys(node.stream_ops); keys.forEach(function (key) { var fn = node.stream_ops[key]; stream_ops[key] = function forceLoadLazyFile() { if (!FS.forceLoadFile(node)) { throw new FS.ErrnoError(5) } return fn.apply(null, arguments) } }); stream_ops.read = function stream_ops_read(stream, buffer, offset, length, position) { if (!FS.forceLoadFile(node)) { throw new FS.ErrnoError(5) } var contents = stream.node.contents; if (position >= contents.length) return 0; var size = Math.min(contents.length - position, length); if (contents.slice) { for (var i = 0; i < size; i++) { buffer[offset + i] = contents[position + i] } } else { for (var i = 0; i < size; i++) { buffer[offset + i] = contents.get(position + i) } } return size }; node.stream_ops = stream_ops; return node }, createPreloadedFile: function (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) { Browser.init(); var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; var dep = getUniqueRunDependency("cp " + fullname); function processData(byteArray) { function finish(byteArray) { if (preFinish) preFinish(); if (!dontCreateFile) { FS.createDataFile(parent, name, byteArray, canRead, canWrite, canOwn) } if (onload) onload(); removeRunDependency(dep) } var handled = false; Module["preloadPlugins"].forEach(function (plugin) { if (handled) return; if (plugin["canHandle"](fullname)) { plugin["handle"](byteArray, fullname, finish, function () { if (onerror) onerror(); removeRunDependency(dep) }); handled = true } }); if (!handled) finish(byteArray) } addRunDependency(dep); if (typeof url == "string") { Browser.asyncLoad(url, function (byteArray) { processData(byteArray) }, onerror) } else { processData(url) } }, indexedDB: function () { return window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB }, DB_NAME: function () { return "EM_FS_" + window.location.pathname }, DB_VERSION: 20, DB_STORE_NAME: "FILE_DATA", saveFilesToDB: function (paths, onload, onerror) { onload = onload || function () { }; onerror = onerror || function () { }; var indexedDB = FS.indexedDB(); try { var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION) } catch (e) { return onerror(e) } openRequest.onupgradeneeded = function openRequest_onupgradeneeded() { console.log("creating db"); var db = openRequest.result; db.createObjectStore(FS.DB_STORE_NAME) }; openRequest.onsuccess = function openRequest_onsuccess() { var db = openRequest.result; var transaction = db.transaction([FS.DB_STORE_NAME], "readwrite"); var files = transaction.objectStore(FS.DB_STORE_NAME); var ok = 0, fail = 0, total = paths.length; function finish() { if (fail == 0) onload(); else onerror() } paths.forEach(function (path) { var putRequest = files.put(FS.analyzePath(path).object.contents, path); putRequest.onsuccess = function putRequest_onsuccess() { ok++; if (ok + fail == total) finish() }; putRequest.onerror = function putRequest_onerror() { fail++; if (ok + fail == total) finish() } }); transaction.onerror = onerror }; openRequest.onerror = onerror }, loadFilesFromDB: function (paths, onload, onerror) { onload = onload || function () { }; onerror = onerror || function () { }; var indexedDB = FS.indexedDB(); try { var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION) } catch (e) { return onerror(e) } openRequest.onupgradeneeded = onerror; openRequest.onsuccess = function openRequest_onsuccess() { var db = openRequest.result; try { var transaction = db.transaction([FS.DB_STORE_NAME], "readonly") } catch (e) { onerror(e); return } var files = transaction.objectStore(FS.DB_STORE_NAME); var ok = 0, fail = 0, total = paths.length; function finish() { if (fail == 0) onload(); else onerror() } paths.forEach(function (path) { var getRequest = files.get(path); getRequest.onsuccess = function getRequest_onsuccess() { if (FS.analyzePath(path).exists) { FS.unlink(path) } FS.createDataFile(PATH.dirname(path), PATH.basename(path), getRequest.result, true, true, true); ok++; if (ok + fail == total) finish() }; getRequest.onerror = function getRequest_onerror() { fail++; if (ok + fail == total) finish() } }); transaction.onerror = onerror }; openRequest.onerror = onerror } }; var SYSCALLS = { DEFAULT_POLLMASK: 5, mappings: {}, umask: 511, calculateAt: function (dirfd, path) { if (path[0] !== "/") { var dir; if (dirfd === -100) { dir = FS.cwd() } else { var dirstream = FS.getStream(dirfd); if (!dirstream) throw new FS.ErrnoError(9); dir = dirstream.path } path = PATH.join2(dir, path) } return path }, doStat: function (func, path, buf) { try { var stat = func(path) } catch (e) { if (e && e.node && PATH.normalize(path) !== PATH.normalize(FS.getPath(e.node))) { return -20 } throw e } HEAP32[buf >> 2] = stat.dev; HEAP32[buf + 4 >> 2] = 0; HEAP32[buf + 8 >> 2] = stat.ino; HEAP32[buf + 12 >> 2] = stat.mode; HEAP32[buf + 16 >> 2] = stat.nlink; HEAP32[buf + 20 >> 2] = stat.uid; HEAP32[buf + 24 >> 2] = stat.gid; HEAP32[buf + 28 >> 2] = stat.rdev; HEAP32[buf + 32 >> 2] = 0; tempI64 = [stat.size >>> 0, (tempDouble = stat.size, +Math_abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0)], HEAP32[buf + 40 >> 2] = tempI64[0], HEAP32[buf + 44 >> 2] = tempI64[1]; HEAP32[buf + 48 >> 2] = 4096; HEAP32[buf + 52 >> 2] = stat.blocks; HEAP32[buf + 56 >> 2] = stat.atime.getTime() / 1e3 | 0; HEAP32[buf + 60 >> 2] = 0; HEAP32[buf + 64 >> 2] = stat.mtime.getTime() / 1e3 | 0; HEAP32[buf + 68 >> 2] = 0; HEAP32[buf + 72 >> 2] = stat.ctime.getTime() / 1e3 | 0; HEAP32[buf + 76 >> 2] = 0; tempI64 = [stat.ino >>> 0, (tempDouble = stat.ino, +Math_abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0)], HEAP32[buf + 80 >> 2] = tempI64[0], HEAP32[buf + 84 >> 2] = tempI64[1]; return 0 }, doMsync: function (addr, stream, len, flags) { var buffer = new Uint8Array(HEAPU8.subarray(addr, addr + len)); FS.msync(stream, buffer, 0, len, flags) }, doMkdir: function (path, mode) { path = PATH.normalize(path); if (path[path.length - 1] === "/") path = path.substr(0, path.length - 1); FS.mkdir(path, mode, 0); return 0 }, doMknod: function (path, mode, dev) { switch (mode & 61440) { case 32768: case 8192: case 24576: case 4096: case 49152: break; default: return -22 }FS.mknod(path, mode, dev); return 0 }, doReadlink: function (path, buf, bufsize) { if (bufsize <= 0) return -22; var ret = FS.readlink(path); var len = Math.min(bufsize, lengthBytesUTF8(ret)); var endChar = HEAP8[buf + len]; stringToUTF8(ret, buf, bufsize + 1); HEAP8[buf + len] = endChar; return len }, doAccess: function (path, amode) { if (amode & ~7) { return -22 } var node; var lookup = FS.lookupPath(path, { follow: true }); node = lookup.node; var perms = ""; if (amode & 4) perms += "r"; if (amode & 2) perms += "w"; if (amode & 1) perms += "x"; if (perms && FS.nodePermissions(node, perms)) { return -13 } return 0 }, doDup: function (path, flags, suggestFD) { var suggest = FS.getStream(suggestFD); if (suggest) FS.close(suggest); return FS.open(path, flags, 0, suggestFD, suggestFD).fd }, doReadv: function (stream, iov, iovcnt, offset) { var ret = 0; for (var i = 0; i < iovcnt; i++) { var ptr = HEAP32[iov + i * 8 >> 2]; var len = HEAP32[iov + (i * 8 + 4) >> 2]; var curr = FS.read(stream, HEAP8, ptr, len, offset); if (curr < 0) return -1; ret += curr; if (curr < len) break } return ret }, doWritev: function (stream, iov, iovcnt, offset) { var ret = 0; for (var i = 0; i < iovcnt; i++) { var ptr = HEAP32[iov + i * 8 >> 2]; var len = HEAP32[iov + (i * 8 + 4) >> 2]; var curr = FS.write(stream, HEAP8, ptr, len, offset); if (curr < 0) return -1; ret += curr } return ret }, varargs: 0, get: function (varargs) { SYSCALLS.varargs += 4; var ret = HEAP32[SYSCALLS.varargs - 4 >> 2]; return ret }, getStr: function () { var ret = UTF8ToString(SYSCALLS.get()); return ret }, getStreamFromFD: function () { var stream = FS.getStream(SYSCALLS.get()); if (!stream) throw new FS.ErrnoError(9); return stream }, get64: function () { var low = SYSCALLS.get(), high = SYSCALLS.get(); return low }, getZero: function () { SYSCALLS.get() } }; function ___syscall140(which, varargs) { SYSCALLS.varargs = varargs; try { var stream = SYSCALLS.getStreamFromFD(), offset_high = SYSCALLS.get(), offset_low = SYSCALLS.get(), result = SYSCALLS.get(), whence = SYSCALLS.get(); if (!(offset_high == -1 && offset_low < 0) && !(offset_high == 0 && offset_low >= 0)) { return -75 } var offset = offset_low; FS.llseek(stream, offset, whence); tempI64 = [stream.position >>> 0, (tempDouble = stream.position, +Math_abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0)], HEAP32[result >> 2] = tempI64[0], HEAP32[result + 4 >> 2] = tempI64[1]; if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null; return 0 } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function ___syscall146(which, varargs) { SYSCALLS.varargs = varargs; try { var stream = SYSCALLS.getStreamFromFD(), iov = SYSCALLS.get(), iovcnt = SYSCALLS.get(); return SYSCALLS.doWritev(stream, iov, iovcnt) } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function ___syscall168(which, varargs) { SYSCALLS.varargs = varargs; try { var fds = SYSCALLS.get(), nfds = SYSCALLS.get(), timeout = SYSCALLS.get(); var nonzero = 0; for (var i = 0; i < nfds; i++) { var pollfd = fds + 8 * i; var fd = HEAP32[pollfd >> 2]; var events = HEAP16[pollfd + 4 >> 1]; var mask = 32; var stream = FS.getStream(fd); if (stream) { mask = SYSCALLS.DEFAULT_POLLMASK; if (stream.stream_ops.poll) { mask = stream.stream_ops.poll(stream) } } mask &= events | 8 | 16; if (mask) nonzero++; HEAP16[pollfd + 6 >> 1] = mask } return nonzero } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function ___syscall195(which, varargs) { SYSCALLS.varargs = varargs; try { var path = SYSCALLS.getStr(), buf = SYSCALLS.get(); return SYSCALLS.doStat(FS.stat, path, buf) } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function ___syscall196(which, varargs) { SYSCALLS.varargs = varargs; try { var path = SYSCALLS.getStr(), buf = SYSCALLS.get(); return SYSCALLS.doStat(FS.lstat, path, buf) } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function ___syscall197(which, varargs) { SYSCALLS.varargs = varargs; try { var stream = SYSCALLS.getStreamFromFD(), buf = SYSCALLS.get(); return SYSCALLS.doStat(FS.stat, stream.path, buf) } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function ___syscall221(which, varargs) { SYSCALLS.varargs = varargs; try { var stream = SYSCALLS.getStreamFromFD(), cmd = SYSCALLS.get(); switch (cmd) { case 0: { var arg = SYSCALLS.get(); if (arg < 0) { return -22 } var newStream; newStream = FS.open(stream.path, stream.flags, 0, arg); return newStream.fd } case 1: case 2: return 0; case 3: return stream.flags; case 4: { var arg = SYSCALLS.get(); stream.flags |= arg; return 0 } case 12: { var arg = SYSCALLS.get(); var offset = 0; HEAP16[arg + offset >> 1] = 2; return 0 } case 13: case 14: return 0; case 16: case 8: return -22; case 9: ___setErrNo(22); return -1; default: { return -22 } } } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function ___syscall3(which, varargs) { SYSCALLS.varargs = varargs; try { var stream = SYSCALLS.getStreamFromFD(), buf = SYSCALLS.get(), count = SYSCALLS.get(); return FS.read(stream, HEAP8, buf, count) } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function ___syscall4(which, varargs) { SYSCALLS.varargs = varargs; try { var stream = SYSCALLS.getStreamFromFD(), buf = SYSCALLS.get(), count = SYSCALLS.get(); return FS.write(stream, HEAP8, buf, count) } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function ___syscall41(which, varargs) { SYSCALLS.varargs = varargs; try { var old = SYSCALLS.getStreamFromFD(); return FS.open(old.path, old.flags, 0).fd } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } var ERRNO_CODES = { EPERM: 1, ENOENT: 2, ESRCH: 3, EINTR: 4, EIO: 5, ENXIO: 6, E2BIG: 7, ENOEXEC: 8, EBADF: 9, ECHILD: 10, EAGAIN: 11, EWOULDBLOCK: 11, ENOMEM: 12, EACCES: 13, EFAULT: 14, ENOTBLK: 15, EBUSY: 16, EEXIST: 17, EXDEV: 18, ENODEV: 19, ENOTDIR: 20, EISDIR: 21, EINVAL: 22, ENFILE: 23, EMFILE: 24, ENOTTY: 25, ETXTBSY: 26, EFBIG: 27, ENOSPC: 28, ESPIPE: 29, EROFS: 30, EMLINK: 31, EPIPE: 32, EDOM: 33, ERANGE: 34, ENOMSG: 42, EIDRM: 43, ECHRNG: 44, EL2NSYNC: 45, EL3HLT: 46, EL3RST: 47, ELNRNG: 48, EUNATCH: 49, ENOCSI: 50, EL2HLT: 51, EDEADLK: 35, ENOLCK: 37, EBADE: 52, EBADR: 53, EXFULL: 54, ENOANO: 55, EBADRQC: 56, EBADSLT: 57, EDEADLOCK: 35, EBFONT: 59, ENOSTR: 60, ENODATA: 61, ETIME: 62, ENOSR: 63, ENONET: 64, ENOPKG: 65, EREMOTE: 66, ENOLINK: 67, EADV: 68, ESRMNT: 69, ECOMM: 70, EPROTO: 71, EMULTIHOP: 72, EDOTDOT: 73, EBADMSG: 74, ENOTUNIQ: 76, EBADFD: 77, EREMCHG: 78, ELIBACC: 79, ELIBBAD: 80, ELIBSCN: 81, ELIBMAX: 82, ELIBEXEC: 83, ENOSYS: 38, ENOTEMPTY: 39, ENAMETOOLONG: 36, ELOOP: 40, EOPNOTSUPP: 95, EPFNOSUPPORT: 96, ECONNRESET: 104, ENOBUFS: 105, EAFNOSUPPORT: 97, EPROTOTYPE: 91, ENOTSOCK: 88, ENOPROTOOPT: 92, ESHUTDOWN: 108, ECONNREFUSED: 111, EADDRINUSE: 98, ECONNABORTED: 103, ENETUNREACH: 101, ENETDOWN: 100, ETIMEDOUT: 110, EHOSTDOWN: 112, EHOSTUNREACH: 113, EINPROGRESS: 115, EALREADY: 114, EDESTADDRREQ: 89, EMSGSIZE: 90, EPROTONOSUPPORT: 93, ESOCKTNOSUPPORT: 94, EADDRNOTAVAIL: 99, ENETRESET: 102, EISCONN: 106, ENOTCONN: 107, ETOOMANYREFS: 109, EUSERS: 87, EDQUOT: 122, ESTALE: 116, ENOTSUP: 95, ENOMEDIUM: 123, EILSEQ: 84, EOVERFLOW: 75, ECANCELED: 125, ENOTRECOVERABLE: 131, EOWNERDEAD: 130, ESTRPIPE: 86 }; var PIPEFS = { BUCKET_BUFFER_SIZE: 8192, mount: function (mount) { return FS.createNode(null, "/", 16384 | 511, 0) }, createPipe: function () { var pipe = { buckets: [] }; pipe.buckets.push({ buffer: new Uint8Array(PIPEFS.BUCKET_BUFFER_SIZE), offset: 0, roffset: 0 }); var rName = PIPEFS.nextname(); var wName = PIPEFS.nextname(); var rNode = FS.createNode(PIPEFS.root, rName, 4096, 0); var wNode = FS.createNode(PIPEFS.root, wName, 4096, 0); rNode.pipe = pipe; wNode.pipe = pipe; var readableStream = FS.createStream({ path: rName, node: rNode, flags: FS.modeStringToFlags("r"), seekable: false, stream_ops: PIPEFS.stream_ops }); rNode.stream = readableStream; var writableStream = FS.createStream({ path: wName, node: wNode, flags: FS.modeStringToFlags("w"), seekable: false, stream_ops: PIPEFS.stream_ops }); wNode.stream = writableStream; return { readable_fd: readableStream.fd, writable_fd: writableStream.fd } }, stream_ops: { poll: function (stream) { var pipe = stream.node.pipe; if ((stream.flags & 2097155) === 1) { return 256 | 4 } else { if (pipe.buckets.length > 0) { for (var i = 0; i < pipe.buckets.length; i++) { var bucket = pipe.buckets[i]; if (bucket.offset - bucket.roffset > 0) { return 64 | 1 } } } } return 0 }, ioctl: function (stream, request, varargs) { return ERRNO_CODES.EINVAL }, read: function (stream, buffer, offset, length, position) { var pipe = stream.node.pipe; var currentLength = 0; for (var i = 0; i < pipe.buckets.length; i++) { var bucket = pipe.buckets[i]; currentLength += bucket.offset - bucket.roffset } assert(buffer instanceof ArrayBuffer || ArrayBuffer.isView(buffer)); var data = buffer.subarray(offset, offset + length); if (length <= 0) { return 0 } if (currentLength == 0) { throw new FS.ErrnoError(ERRNO_CODES.EAGAIN) } var toRead = Math.min(currentLength, length); var totalRead = toRead; var toRemove = 0; for (var i = 0; i < pipe.buckets.length; i++) { var currBucket = pipe.buckets[i]; var bucketSize = currBucket.offset - currBucket.roffset; if (toRead <= bucketSize) { var tmpSlice = currBucket.buffer.subarray(currBucket.roffset, currBucket.offset); if (toRead < bucketSize) { tmpSlice = tmpSlice.subarray(0, toRead); currBucket.roffset += toRead } else { toRemove++ } data.set(tmpSlice); break } else { var tmpSlice = currBucket.buffer.subarray(currBucket.roffset, currBucket.offset); data.set(tmpSlice); data = data.subarray(tmpSlice.byteLength); toRead -= tmpSlice.byteLength; toRemove++ } } if (toRemove && toRemove == pipe.buckets.length) { toRemove--; pipe.buckets[toRemove].offset = 0; pipe.buckets[toRemove].roffset = 0 } pipe.buckets.splice(0, toRemove); return totalRead }, write: function (stream, buffer, offset, length, position) { var pipe = stream.node.pipe; assert(buffer instanceof ArrayBuffer || ArrayBuffer.isView(buffer)); var data = buffer.subarray(offset, offset + length); var dataLen = data.byteLength; if (dataLen <= 0) { return 0 } var currBucket = null; if (pipe.buckets.length == 0) { currBucket = { buffer: new Uint8Array(PIPEFS.BUCKET_BUFFER_SIZE), offset: 0, roffset: 0 }; pipe.buckets.push(currBucket) } else { currBucket = pipe.buckets[pipe.buckets.length - 1] } assert(currBucket.offset <= PIPEFS.BUCKET_BUFFER_SIZE); var freeBytesInCurrBuffer = PIPEFS.BUCKET_BUFFER_SIZE - currBucket.offset; if (freeBytesInCurrBuffer >= dataLen) { currBucket.buffer.set(data, currBucket.offset); currBucket.offset += dataLen; return dataLen } else if (freeBytesInCurrBuffer > 0) { currBucket.buffer.set(data.subarray(0, freeBytesInCurrBuffer), currBucket.offset); currBucket.offset += freeBytesInCurrBuffer; data = data.subarray(freeBytesInCurrBuffer, data.byteLength) } var numBuckets = data.byteLength / PIPEFS.BUCKET_BUFFER_SIZE | 0; var remElements = data.byteLength % PIPEFS.BUCKET_BUFFER_SIZE; for (var i = 0; i < numBuckets; i++) { var newBucket = { buffer: new Uint8Array(PIPEFS.BUCKET_BUFFER_SIZE), offset: PIPEFS.BUCKET_BUFFER_SIZE, roffset: 0 }; pipe.buckets.push(newBucket); newBucket.buffer.set(data.subarray(0, PIPEFS.BUCKET_BUFFER_SIZE)); data = data.subarray(PIPEFS.BUCKET_BUFFER_SIZE, data.byteLength) } if (remElements > 0) { var newBucket = { buffer: new Uint8Array(PIPEFS.BUCKET_BUFFER_SIZE), offset: data.byteLength, roffset: 0 }; pipe.buckets.push(newBucket); newBucket.buffer.set(data) } return dataLen }, close: function (stream) { var pipe = stream.node.pipe; pipe.buckets = null } }, nextname: function () { if (!PIPEFS.nextname.current) { PIPEFS.nextname.current = 0 } return "pipe[" + PIPEFS.nextname.current++ + "]" } }; function ___syscall42(which, varargs) { SYSCALLS.varargs = varargs; try { var fdPtr = SYSCALLS.get(); if (fdPtr == 0) { throw new FS.ErrnoError(14) } var res = PIPEFS.createPipe(); HEAP32[fdPtr >> 2] = res.readable_fd; HEAP32[fdPtr + 4 >> 2] = res.writable_fd; return 0 } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function ___syscall5(which, varargs) { SYSCALLS.varargs = varargs; try { var pathname = SYSCALLS.getStr(), flags = SYSCALLS.get(), mode = SYSCALLS.get(); var stream = FS.open(pathname, flags, mode); return stream.fd } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function ___syscall6(which, varargs) { SYSCALLS.varargs = varargs; try { var stream = SYSCALLS.getStreamFromFD(); FS.close(stream); return 0 } catch (e) { if (typeof FS === "undefined" || !(e instanceof FS.ErrnoError)) abort(e); return -e.errno } } function _abort() { Module["abort"]() } function _emscripten_get_heap_size() { return HEAP8.length } function abortOnCannotGrowMemory(requestedSize) { abort("OOM") } function emscripten_realloc_buffer(size) { var PAGE_MULTIPLE = 65536; size = alignUp(size, PAGE_MULTIPLE); var oldSize = buffer.byteLength; try { var result = wasmMemory.grow((size - oldSize) / 65536); if (result !== (-1 | 0)) { buffer = wasmMemory.buffer; return true } else { return false } } catch (e) { return false } } function _emscripten_resize_heap(requestedSize) { var oldSize = _emscripten_get_heap_size(); var PAGE_MULTIPLE = 65536; var LIMIT = 2147483648 - PAGE_MULTIPLE; if (requestedSize > LIMIT) { return false } var MIN_TOTAL_MEMORY = 16777216; var newSize = Math.max(oldSize, MIN_TOTAL_MEMORY); while (newSize < requestedSize) { if (newSize <= 536870912) { newSize = alignUp(2 * newSize, PAGE_MULTIPLE) } else { newSize = Math.min(alignUp((3 * newSize + 2147483648) / 4, PAGE_MULTIPLE), LIMIT) } } if (!emscripten_realloc_buffer(newSize)) { return false } updateGlobalBufferViews(); return true } function _exit(status) { exit(status) } var ___tm_current = 277408; var ___tm_timezone = (stringToUTF8("GMT", 277456, 4), 277456); function _tzset() { if (_tzset.called) return; _tzset.called = true; HEAP32[__get_timezone() >> 2] = (new Date).getTimezoneOffset() * 60; var winter = new Date(2e3, 0, 1); var summer = new Date(2e3, 6, 1); HEAP32[__get_daylight() >> 2] = Number(winter.getTimezoneOffset() != summer.getTimezoneOffset()); function extractZone(date) { var match = date.toTimeString().match(/\(([A-Za-z ]+)\)$/); return match ? match[1] : "GMT" } var winterName = extractZone(winter); var summerName = extractZone(summer); var winterNamePtr = allocate(intArrayFromString(winterName), "i8", ALLOC_NORMAL); var summerNamePtr = allocate(intArrayFromString(summerName), "i8", ALLOC_NORMAL); if (summer.getTimezoneOffset() < winter.getTimezoneOffset()) { HEAP32[__get_tzname() >> 2] = winterNamePtr; HEAP32[__get_tzname() + 4 >> 2] = summerNamePtr } else { HEAP32[__get_tzname() >> 2] = summerNamePtr; HEAP32[__get_tzname() + 4 >> 2] = winterNamePtr } } function _localtime_r(time, tmPtr) { _tzset(); var date = new Date(HEAP32[time >> 2] * 1e3); HEAP32[tmPtr >> 2] = date.getSeconds(); HEAP32[tmPtr + 4 >> 2] = date.getMinutes(); HEAP32[tmPtr + 8 >> 2] = date.getHours(); HEAP32[tmPtr + 12 >> 2] = date.getDate(); HEAP32[tmPtr + 16 >> 2] = date.getMonth(); HEAP32[tmPtr + 20 >> 2] = date.getFullYear() - 1900; HEAP32[tmPtr + 24 >> 2] = date.getDay(); var start = new Date(date.getFullYear(), 0, 1); var yday = (date.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24) | 0; HEAP32[tmPtr + 28 >> 2] = yday; HEAP32[tmPtr + 36 >> 2] = -(date.getTimezoneOffset() * 60); var summerOffset = new Date(2e3, 6, 1).getTimezoneOffset(); var winterOffset = start.getTimezoneOffset(); var dst = (summerOffset != winterOffset && date.getTimezoneOffset() == Math.min(winterOffset, summerOffset)) | 0; HEAP32[tmPtr + 32 >> 2] = dst; var zonePtr = HEAP32[__get_tzname() + (dst ? 4 : 0) >> 2]; HEAP32[tmPtr + 40 >> 2] = zonePtr; return tmPtr } function _localtime(time) { return _localtime_r(time, ___tm_current) } function _emscripten_memcpy_big(dest, src, num) { HEAPU8.set(HEAPU8.subarray(src, src + num), dest) } function _mktime(tmPtr) { _tzset(); var date = new Date(HEAP32[tmPtr + 20 >> 2] + 1900, HEAP32[tmPtr + 16 >> 2], HEAP32[tmPtr + 12 >> 2], HEAP32[tmPtr + 8 >> 2], HEAP32[tmPtr + 4 >> 2], HEAP32[tmPtr >> 2], 0); var dst = HEAP32[tmPtr + 32 >> 2]; var guessedOffset = date.getTimezoneOffset(); var start = new Date(date.getFullYear(), 0, 1); var summerOffset = new Date(2e3, 6, 1).getTimezoneOffset(); var winterOffset = start.getTimezoneOffset(); var dstOffset = Math.min(winterOffset, summerOffset); if (dst < 0) { HEAP32[tmPtr + 32 >> 2] = Number(summerOffset != winterOffset && dstOffset == guessedOffset) } else if (dst > 0 != (dstOffset == guessedOffset)) { var nonDstOffset = Math.max(winterOffset, summerOffset); var trueOffset = dst > 0 ? dstOffset : nonDstOffset; date.setTime(date.getTime() + (trueOffset - guessedOffset) * 6e4) } HEAP32[tmPtr + 24 >> 2] = date.getDay(); var yday = (date.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24) | 0; HEAP32[tmPtr + 28 >> 2] = yday; return date.getTime() / 1e3 | 0 } function _posix_spawn_file_actions_addclose() { err("missing function: posix_spawn_file_actions_addclose"); abort(-1) } function _posix_spawn_file_actions_adddup2() { err("missing function: posix_spawn_file_actions_adddup2"); abort(-1) } function _posix_spawn_file_actions_destroy() { err("missing function: posix_spawn_file_actions_destroy"); abort(-1) } function _posix_spawn_file_actions_init() { err("missing function: posix_spawn_file_actions_init"); abort(-1) } function _fork() { ___setErrNo(11); return -1 } function _posix_spawnp() { return _fork.apply(null, arguments) } function _timegm(tmPtr) { _tzset(); var time = Date.UTC(HEAP32[tmPtr + 20 >> 2] + 1900, HEAP32[tmPtr + 16 >> 2], HEAP32[tmPtr + 12 >> 2], HEAP32[tmPtr + 8 >> 2], HEAP32[tmPtr + 4 >> 2], HEAP32[tmPtr >> 2], 0); var date = new Date(time); HEAP32[tmPtr + 24 >> 2] = date.getUTCDay(); var start = Date.UTC(date.getUTCFullYear(), 0, 1, 0, 0, 0, 0); var yday = (date.getTime() - start) / (1e3 * 60 * 60 * 24) | 0; HEAP32[tmPtr + 28 >> 2] = yday; return date.getTime() / 1e3 | 0 } function _wait(stat_loc) { ___setErrNo(10); return -1 } function _waitpid() { return _wait.apply(null, arguments) } FS.staticInit(); if (ENVIRONMENT_IS_NODE) { var fs = require("fs"); var NODEJS_PATH = require("path"); NODEFS.staticInit() } function intArrayFromString(stringy, dontAddNull, length) { var len = length > 0 ? length : lengthBytesUTF8(stringy) + 1; var u8array = new Array(len); var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); if (dontAddNull) u8array.length = numBytesWritten; return u8array } var asmGlobalArg = {}; var asmLibraryArg = { "b": abort, "q": setTempRet0, "G": ___buildEnvironment, "l": ___setErrNo, "s": ___syscall140, "i": ___syscall146, "p": ___syscall168, "o": ___syscall195, "n": ___syscall196, "m": ___syscall197, "c": ___syscall221, "F": ___syscall3, "E": ___syscall4, "D": ___syscall41, "C": ___syscall42, "B": ___syscall5, "h": ___syscall6, "g": _abort, "A": _emscripten_get_heap_size, "z": _emscripten_memcpy_big, "y": _emscripten_resize_heap, "f": _exit, "x": _localtime, "d": _mktime, "e": _posix_spawn_file_actions_addclose, "k": _posix_spawn_file_actions_adddup2, "j": _posix_spawn_file_actions_destroy, "w": _posix_spawn_file_actions_init, "v": _posix_spawnp, "u": _timegm, "t": _waitpid, "r": abortOnCannotGrowMemory, "a": DYNAMICTOP_PTR }; var asm = Module["asm"](asmGlobalArg, asmLibraryArg, buffer); Module["asm"] = asm; var ___emscripten_environ_constructor = Module["___emscripten_environ_constructor"] = function () { return Module["asm"]["H"].apply(null, arguments) }; var ___errno_location = Module["___errno_location"] = function () { return Module["asm"]["I"].apply(null, arguments) }; var __get_daylight = Module["__get_daylight"] = function () { return Module["asm"]["J"].apply(null, arguments) }; var __get_timezone = Module["__get_timezone"] = function () { return Module["asm"]["K"].apply(null, arguments) }; var __get_tzname = Module["__get_tzname"] = function () { return Module["asm"]["L"].apply(null, arguments) }; var _archive_close = Module["_archive_close"] = function () { return Module["asm"]["M"].apply(null, arguments) }; var _archive_entry_filetype = Module["_archive_entry_filetype"] = function () { return Module["asm"]["N"].apply(null, arguments) }; var _archive_entry_is_encrypted = Module["_archive_entry_is_encrypted"] = function () { return Module["asm"]["O"].apply(null, arguments) }; var _archive_entry_pathname = Module["_archive_entry_pathname"] = function () { return Module["asm"]["P"].apply(null, arguments) }; var _archive_entry_pathname_utf8 = Module["_archive_entry_pathname_utf8"] = function () { return Module["asm"]["Q"].apply(null, arguments) }; var _archive_entry_size = Module["_archive_entry_size"] = function () { return Module["asm"]["R"].apply(null, arguments) }; var _archive_error_string = Module["_archive_error_string"] = function () { return Module["asm"]["S"].apply(null, arguments) }; var _archive_open = Module["_archive_open"] = function () { return Module["asm"]["T"].apply(null, arguments) }; var _archive_read_add_passphrase = Module["_archive_read_add_passphrase"] = function () { return Module["asm"]["U"].apply(null, arguments) }; var _archive_read_data_skip = Module["_archive_read_data_skip"] = function () { return Module["asm"]["V"].apply(null, arguments) }; var _archive_read_has_encrypted_entries = Module["_archive_read_has_encrypted_entries"] = function () { return Module["asm"]["W"].apply(null, arguments) }; var _free = Module["_free"] = function () { return Module["asm"]["X"].apply(null, arguments) }; var _get_filedata = Module["_get_filedata"] = function () { return Module["asm"]["Y"].apply(null, arguments) }; var _get_next_entry = Module["_get_next_entry"] = function () { return Module["asm"]["Z"].apply(null, arguments) }; var _get_version = Module["_get_version"] = function () { return Module["asm"]["_"].apply(null, arguments) }; var _malloc = Module["_malloc"] = function () { return Module["asm"]["$"].apply(null, arguments) }; var stackAlloc = Module["stackAlloc"] = function () { return Module["asm"]["ca"].apply(null, arguments) }; var stackRestore = Module["stackRestore"] = function () { return Module["asm"]["da"].apply(null, arguments) }; var stackSave = Module["stackSave"] = function () { return Module["asm"]["ea"].apply(null, arguments) }; var dynCall_v = Module["dynCall_v"] = function () { return Module["asm"]["aa"].apply(null, arguments) }; var dynCall_vi = Module["dynCall_vi"] = function () { return Module["asm"]["ba"].apply(null, arguments) }; Module["asm"] = asm; Module["intArrayFromString"] = intArrayFromString; Module["cwrap"] = cwrap; Module["allocate"] = allocate; Module["then"] = function (func) { if (Module["calledRun"]) { func(Module) } else { var old = Module["onRuntimeInitialized"]; Module["onRuntimeInitialized"] = function () { if (old) old(); func(Module) } } return Module }; function ExitStatus(status) { this.name = "ExitStatus"; this.message = "Program terminated with exit(" + status + ")"; this.status = status } ExitStatus.prototype = new Error; ExitStatus.prototype.constructor = ExitStatus; dependenciesFulfilled = function runCaller() { if (!Module["calledRun"]) run(); if (!Module["calledRun"]) dependenciesFulfilled = runCaller }; function run(args) { args = args || Module["arguments"]; if (runDependencies > 0) { return } preRun(); if (runDependencies > 0) return; if (Module["calledRun"]) return; function doRun() { if (Module["calledRun"]) return; Module["calledRun"] = true; if (ABORT) return; ensureInitRuntime(); preMain(); if (Module["onRuntimeInitialized"]) Module["onRuntimeInitialized"](); postRun() } if (Module["setStatus"]) { Module["setStatus"]("Running..."); setTimeout(function () { setTimeout(function () { Module["setStatus"]("") }, 1); doRun() }, 1) } else { doRun() } } Module["run"] = run; function exit(status, implicit) { if (implicit && Module["noExitRuntime"] && status === 0) { return } if (Module["noExitRuntime"]) { } else { ABORT = true; EXITSTATUS = status; exitRuntime(); if (Module["onExit"]) Module["onExit"](status) } Module["quit"](status, new ExitStatus(status)) } function abort(what) { if (Module["onAbort"]) { Module["onAbort"](what) } if (what !== undefined) { out(what); err(what); what = JSON.stringify(what) } else { what = "" } ABORT = true; EXITSTATUS = 1; throw "abort(" + what + "). Build with -s ASSERTIONS=1 for more info." } Module["abort"] = abort; if (Module["preInit"]) { if (typeof Module["preInit"] == "function") Module["preInit"] = [Module["preInit"]]; while (Module["preInit"].length > 0) { Module["preInit"].pop()() } } Module["noExitRuntime"] = true; run() + + + return libarchive + } + ) +})() +module.exports = libarchive \ No newline at end of file diff --git a/server/libs/libarchive/wasm-module.js b/server/libs/libarchive/wasm-module.js new file mode 100644 index 00000000..2e3a4ec0 --- /dev/null +++ b/server/libs/libarchive/wasm-module.js @@ -0,0 +1,235 @@ +/** + * Modified from https://github.com/nika-begiashvili/libarchivejs + */ + +const Path = require('path') +const libarchive = require('./wasm-libarchive') + +const TYPE_MAP = { + 32768: 'FILE', + 16384: 'DIR', + 40960: 'SYMBOLIC_LINK', + 49152: 'SOCKET', + 8192: 'CHARACTER_DEVICE', + 24576: 'BLOCK_DEVICE', + 4096: 'NAMED_PIPE', +} + +class ArchiveReader { + /** + * archive reader + * @param {WasmModule} wasmModule emscripten module + */ + constructor(wasmModule) { + this._wasmModule = wasmModule + this._runCode = wasmModule.runCode + this._file = null + this._passphrase = null + } + + /** + * open archive, needs to closed manually + * @param {File} file + */ + open(file) { + if (this._file !== null) { + console.warn('Closing previous file') + this.close() + } + const { promise, resolve, reject } = this._promiseHandles() + this._file = file + this._loadFile(file, resolve, reject) + return promise + } + + /** + * close archive + */ + close() { + this._runCode.closeArchive(this._archive) + this._wasmModule._free(this._filePtr) + this._file = null + this._filePtr = null + this._archive = null + } + + /** + * detect if archive has encrypted data + * @returns {boolean|null} null if could not be determined + */ + hasEncryptedData() { + this._archive = this._runCode.openArchive(this._filePtr, this._fileLength, this._passphrase) + this._runCode.getNextEntry(this._archive) + const status = this._runCode.hasEncryptedEntries(this._archive) + if (status === 0) { + return false + } else if (status > 0) { + return true + } else { + return null + } + } + + /** + * set passphrase to be used with archive + * @param {*} passphrase + */ + setPassphrase(passphrase) { + this._passphrase = passphrase + } + + /** + * get archive entries + * @param {boolean} skipExtraction + * @param {string} except don't skip this entry + */ + *entries(skipExtraction = false, except = null) { + this._archive = this._runCode.openArchive(this._filePtr, this._fileLength, this._passphrase) + let entry + while (true) { + entry = this._runCode.getNextEntry(this._archive) + if (entry === 0) break + + const entryData = { + size: this._runCode.getEntrySize(entry), + path: this._runCode.getEntryName(entry), + type: TYPE_MAP[this._runCode.getEntryType(entry)], + ref: entry, + } + + if (entryData.type === 'FILE') { + let fileName = entryData.path.split('/') + entryData.fileName = fileName[fileName.length - 1] + } + + if (skipExtraction && except !== entryData.path) { + this._runCode.skipEntry(this._archive) + } else { + const ptr = this._runCode.getFileData(this._archive, entryData.size) + if (ptr < 0) { + throw new Error(this._runCode.getError(this._archive)) + } + entryData.fileData = this._wasmModule.HEAP8.slice(ptr, ptr + entryData.size) + this._wasmModule._free(ptr) + } + yield entryData + } + } + + _loadFile(fileBuffer, resolve, reject) { + try { + const array = new Uint8Array(fileBuffer) + this._fileLength = array.length + this._filePtr = this._runCode.malloc(this._fileLength) + this._wasmModule.HEAP8.set(array, this._filePtr) + resolve() + } catch (error) { + reject(error) + } + } + + _promiseHandles() { + let resolve = null, reject = null + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve + reject = _reject + }) + return { promise, resolve, reject } + } + +} + +class WasmModule { + constructor() { + this.preRun = [] + this.postRun = [] + this.totalDependencies = 0 + } + + print(...text) { + console.log(text) + } + + printErr(...text) { + console.error(text) + } + + initFunctions() { + this.runCode = { + // const char * get_version() + getVersion: this.cwrap('get_version', 'string', []), + // void * archive_open( const void * buffer, size_t buffer_size) + // retuns archive pointer + openArchive: this.cwrap('archive_open', 'number', ['number', 'number', 'string']), + // void * get_entry(void * archive) + // return archive entry pointer + getNextEntry: this.cwrap('get_next_entry', 'number', ['number']), + // void * get_filedata( void * archive, size_t bufferSize ) + getFileData: this.cwrap('get_filedata', 'number', ['number', 'number']), + // int archive_read_data_skip(struct archive *_a) + skipEntry: this.cwrap('archive_read_data_skip', 'number', ['number']), + // void archive_close( void * archive ) + closeArchive: this.cwrap('archive_close', null, ['number']), + // la_int64_t archive_entry_size( struct archive_entry * ) + getEntrySize: this.cwrap('archive_entry_size', 'number', ['number']), + // const char * archive_entry_pathname( struct archive_entry * ) + getEntryName: this.cwrap('archive_entry_pathname', 'string', ['number']), + // __LA_MODE_T archive_entry_filetype( struct archive_entry * ) + /* + #define AE_IFMT ((__LA_MODE_T)0170000) + #define AE_IFREG ((__LA_MODE_T)0100000) // Regular file + #define AE_IFLNK ((__LA_MODE_T)0120000) // Sybolic link + #define AE_IFSOCK ((__LA_MODE_T)0140000) // Socket + #define AE_IFCHR ((__LA_MODE_T)0020000) // Character device + #define AE_IFBLK ((__LA_MODE_T)0060000) // Block device + #define AE_IFDIR ((__LA_MODE_T)0040000) // Directory + #define AE_IFIFO ((__LA_MODE_T)0010000) // Named pipe + */ + getEntryType: this.cwrap('archive_entry_filetype', 'number', ['number']), + // const char * archive_error_string(struct archive *); + getError: this.cwrap('archive_error_string', 'string', ['number']), + + /* + * Returns 1 if the archive contains at least one encrypted entry. + * If the archive format not support encryption at all + * ARCHIVE_READ_FORMAT_ENCRYPTION_UNSUPPORTED is returned. + * If for any other reason (e.g. not enough data read so far) + * we cannot say whether there are encrypted entries, then + * ARCHIVE_READ_FORMAT_ENCRYPTION_DONT_KNOW is returned. + * In general, this function will return values below zero when the + * reader is uncertain or totally incapable of encryption support. + * When this function returns 0 you can be sure that the reader + * supports encryption detection but no encrypted entries have + * been found yet. + * + * NOTE: If the metadata/header of an archive is also encrypted, you + * cannot rely on the number of encrypted entries. That is why this + * function does not return the number of encrypted entries but# + * just shows that there are some. + */ + // __LA_DECL int archive_read_has_encrypted_entries(struct archive *); + entryIsEncrypted: this.cwrap('archive_entry_is_encrypted', 'number', ['number']), + hasEncryptedEntries: this.cwrap('archive_read_has_encrypted_entries', 'number', ['number']), + // __LA_DECL int archive_read_add_passphrase(struct archive *, const char *); + addPassphrase: this.cwrap('archive_read_add_passphrase', 'number', ['number', 'string']), + //this.stringToUTF(str), // + string: (str) => this.allocate(this.intArrayFromString(str), 'i8', 0), + malloc: this.cwrap('malloc', 'number', ['number']), + free: this.cwrap('free', null, ['number']), + } + } + + monitorRunDependencies() { } + + locateFile(path /* ,prefix */) { + const wasmFilepath = Path.join(__dirname, `../../../client/dist/libarchive/wasm-gen/${path}`) + return wasmFilepath + } +} + +module.exports.getArchiveReader = (cb) => { + libarchive(new WasmModule()).then((module) => { + module.initFunctions() + cb(new ArchiveReader(module)) + }) +} \ No newline at end of file diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index b40e9323..85bf8146 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -681,7 +681,7 @@ class BookScanner { const bookTitle = this.bookMetadata.title || this.libraryItemData.mediaMetadata.title AudioFileScanner.setBookMetadataFromAudioMetaTags(bookTitle, this.audioFiles, this.bookMetadata, this.libraryScan) } else if (this.ebookFileScanData) { - const ebookMetdataObject = this.ebookFileScanData.metadata + const ebookMetdataObject = this.ebookFileScanData.metadata || {} for (const key in ebookMetdataObject) { if (key === 'tags') { if (ebookMetdataObject.tags.length) { diff --git a/server/utils/parsers/parseComicInfoMetadata.js b/server/utils/parsers/parseComicInfoMetadata.js new file mode 100644 index 00000000..e2bad4ec --- /dev/null +++ b/server/utils/parsers/parseComicInfoMetadata.js @@ -0,0 +1,35 @@ + +/** + * TODO: Add more fields + * @see https://anansi-project.github.io/docs/comicinfo/intro + * + * @param {Object} comicInfoJson + * @returns {import('../../scanner/BookScanner').BookMetadataObject} + */ +module.exports.parse = (comicInfoJson) => { + if (!comicInfoJson?.ComicInfo) return null + + const ComicSeries = comicInfoJson.ComicInfo.Series?.[0]?.trim() || null + const ComicNumber = comicInfoJson.ComicInfo.Number?.[0]?.trim() || null + const ComicSummary = comicInfoJson.ComicInfo.Summary?.[0]?.trim() || null + + let title = null + const series = [] + if (ComicSeries) { + series.push({ + name: ComicSeries, + sequence: ComicNumber + }) + + title = ComicSeries + if (ComicNumber) { + title += ` ${ComicNumber}` + } + } + + return { + title, + series, + description: ComicSummary + } +} \ No newline at end of file diff --git a/server/utils/parsers/parseComicMetadata.js b/server/utils/parsers/parseComicMetadata.js new file mode 100644 index 00000000..8d78b126 --- /dev/null +++ b/server/utils/parsers/parseComicMetadata.js @@ -0,0 +1,109 @@ +const Path = require('path') +const globals = require('../globals') +const fs = require('../../libs/fsExtra') +const Logger = require('../../Logger') +const Archive = require('../../libs/libarchive/archive') +const { xmlToJSON } = require('../index') +const parseComicInfoMetadata = require('./parseComicInfoMetadata') + +/** + * + * @param {string} filepath + * @returns {Promise<Buffer>} + */ +async function getComicFileBuffer(filepath) { + if (!await fs.pathExists(filepath)) { + Logger.error(`Comic path does not exist "${filepath}"`) + return null + } + try { + return fs.readFile(filepath) + } catch (error) { + Logger.error(`Failed to read comic at "${filepath}"`, error) + return null + } +} + +/** + * Extract cover image from comic return true if success + * + * @param {string} comicPath + * @param {string} comicImageFilepath + * @param {string} outputCoverPath + * @returns {Promise<boolean>} + */ +async function extractCoverImage(comicPath, comicImageFilepath, outputCoverPath) { + const comicFileBuffer = await getComicFileBuffer(comicPath) + if (!comicFileBuffer) return null + + const archive = await Archive.open(comicFileBuffer) + const fileEntry = await archive.extractSingleFile(comicImageFilepath) + + if (!fileEntry?.fileData) { + Logger.error(`[parseComicMetadata] Invalid file entry data for comicPath "${comicPath}"/${comicImageFilepath}`) + return false + } + + try { + await fs.writeFile(outputCoverPath, fileEntry.fileData) + return true + } catch (error) { + Logger.error(`[parseComicMetadata] Failed to extract image from comicPath "${comicPath}"`, error) + return false + } +} +module.exports.extractCoverImage = extractCoverImage + +/** + * Parse metadata from comic + * + * @param {import('../../models/Book').EBookFileObject} ebookFile + * @returns {Promise<import('./parseEbookMetadata').EBookFileScanData>} + */ +async function parse(ebookFile) { + const comicPath = ebookFile.metadata.path + Logger.debug(`Parsing metadata from comic at "${comicPath}"`) + + const comicFileBuffer = await getComicFileBuffer(comicPath) + if (!comicFileBuffer) return null + + const archive = await Archive.open(comicFileBuffer) + + const fileObjects = await archive.getFilesArray() + + fileObjects.sort((a, b) => { + return a.file.name.localeCompare(b.file.name, undefined, { + numeric: true, + sensitivity: 'base' + }) + }) + + let metadata = null + const comicInfo = fileObjects.find(fo => fo.file.name === 'ComicInfo.xml') + if (comicInfo) { + const comicInfoEntry = await comicInfo.file.extract() + if (comicInfoEntry?.fileData) { + const comicInfoStr = new TextDecoder().decode(comicInfoEntry.fileData) + const comicInfoJson = await xmlToJSON(comicInfoStr) + if (comicInfoJson) { + metadata = parseComicInfoMetadata.parse(comicInfoJson) + } + } + } + + const payload = { + path: comicPath, + ebookFormat: ebookFile.ebookFormat, + metadata + } + + const firstImage = fileObjects.find(fo => globals.SupportedImageTypes.includes(Path.extname(fo.file.name).toLowerCase().slice(1))) + if (firstImage?.file?._path) { + payload.ebookCoverPath = firstImage.file._path + } else { + Logger.warn(`Cover image not found in comic at "${comicPath}"`) + } + + return payload +} +module.exports.parse = parse \ No newline at end of file diff --git a/server/utils/parsers/parseEbookMetadata.js b/server/utils/parsers/parseEbookMetadata.js index 6e97c1da..9ef7861c 100644 --- a/server/utils/parsers/parseEbookMetadata.js +++ b/server/utils/parsers/parseEbookMetadata.js @@ -1,4 +1,5 @@ const parseEpubMetadata = require('./parseEpubMetadata') +const parseComicMetadata = require('./parseComicMetadata') /** * @typedef EBookFileScanData @@ -18,7 +19,9 @@ async function parse(ebookFile) { if (!ebookFile) return null if (ebookFile.ebookFormat === 'epub') { - return parseEpubMetadata.parse(ebookFile.metadata.path) + return parseEpubMetadata.parse(ebookFile) + } else if (['cbz', 'cbr'].includes(ebookFile.ebookFormat)) { + return parseComicMetadata.parse(ebookFile) } return null } @@ -36,6 +39,8 @@ async function extractCoverImage(ebookFileScanData, outputCoverPath) { if (ebookFileScanData.ebookFormat === 'epub') { return parseEpubMetadata.extractCoverImage(ebookFileScanData.path, ebookFileScanData.ebookCoverPath, outputCoverPath) + } else if (['cbz', 'cbr'].includes(ebookFileScanData.ebookFormat)) { + return parseComicMetadata.extractCoverImage(ebookFileScanData.path, ebookFileScanData.ebookCoverPath, outputCoverPath) } return false } diff --git a/server/utils/parsers/parseEpubMetadata.js b/server/utils/parsers/parseEpubMetadata.js index 7238b0bf..47330046 100644 --- a/server/utils/parsers/parseEpubMetadata.js +++ b/server/utils/parsers/parseEpubMetadata.js @@ -60,10 +60,11 @@ module.exports.extractCoverImage = extractCoverImage /** * Parse metadata from epub * - * @param {string} epubPath + * @param {import('../../models/Book').EBookFileObject} ebookFile * @returns {Promise<import('./parseEbookMetadata').EBookFileScanData>} */ -async function parse(epubPath) { +async function parse(ebookFile) { + const epubPath = ebookFile.metadata.path Logger.debug(`Parsing metadata from epub at "${epubPath}"`) // Entrypoint of the epub that contains the filepath to the package document (opf file) const containerJson = await extractXmlToJson(epubPath, 'META-INF/container.xml') From b51853b3df3f0573e8aef5613539ad5ea22a9ce6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 15 Jan 2024 08:34:12 -0600 Subject: [PATCH 0310/2145] Update:Use raw cover art for media session #2514 --- client/components/app/StreamContainer.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index e9b6969d..0cae1421 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -349,7 +349,7 @@ export default { } if ('mediaSession' in navigator) { - var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png') + var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true) const artwork = [ { src: coverImageSrc From 7b0fa48e2e7ab93a308886248611eb057d1273ff Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 16 Jan 2024 16:31:16 -0600 Subject: [PATCH 0311/2145] Update jsdocs for expanded library items --- server/models/Book.js | 15 ++++++++++ server/models/LibraryItem.js | 56 ++++++++++++++++++++++++++++++++++++ server/models/Podcast.js | 7 +++++ 3 files changed, 78 insertions(+) diff --git a/server/models/Book.js b/server/models/Book.js index 9537d7b3..6b179c36 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -18,6 +18,19 @@ const Logger = require('../Logger') * @property {string} title */ +/** + * @typedef SeriesExpandedProperties + * @property {{sequence:string}} bookSeries + * + * @typedef {import('./Series') & SeriesExpandedProperties} SeriesExpanded + * + * @typedef BookExpandedProperties + * @property {import('./Author')[]} authors + * @property {SeriesExpanded[]} series + * + * @typedef {Book & BookExpandedProperties} BookExpanded + */ + /** * @typedef AudioFileObject * @property {number} index @@ -54,6 +67,8 @@ class Book extends Model { /** @type {string} */ this.titleIgnorePrefix /** @type {string} */ + this.subtitle + /** @type {string} */ this.publishedYear /** @type {string} */ this.publishedDate diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 508cf4c6..ee8a4bb8 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -15,6 +15,13 @@ const Podcast = require('./Podcast') * @property {{filename:string, ext:string, path:string, relPath:string, size:number, mtimeMs:number, ctimeMs:number, birthtimeMs:number}} metadata */ +/** + * @typedef LibraryItemExpandedProperties + * @property {Book.BookExpanded|Podcast.PodcastExpanded} media + * + * @typedef {LibraryItem & LibraryItemExpandedProperties} LibraryItemExpanded + */ + class LibraryItem extends Model { constructor(values, options) { super(values, options) @@ -412,6 +419,55 @@ class LibraryItem extends Model { }) } + /** + * + * @param {string} libraryItemId + * @returns {Promise<LibraryItemExpanded>} + */ + static async getExpandedById(libraryItemId) { + if (!libraryItemId) return null + + const libraryItem = await this.findByPk(libraryItemId) + if (!libraryItem) { + Logger.error(`[LibraryItem] Library item not found with id "${libraryItemId}"`) + return null + } + + if (libraryItem.mediaType === 'podcast') { + libraryItem.media = await libraryItem.getMedia({ + include: [ + { + model: this.sequelize.models.podcastEpisode + } + ] + }) + } else { + libraryItem.media = await libraryItem.getMedia({ + include: [ + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + } + ], + order: [ + [this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'], + [this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'] + ] + }) + } + + if (!libraryItem.media) return null + return libraryItem + } + /** * Get old library item by id * @param {string} libraryItemId diff --git a/server/models/Podcast.js b/server/models/Podcast.js index 82ae8fe2..940ae0ab 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -1,5 +1,12 @@ const { DataTypes, Model } = require('sequelize') +/** + * @typedef PodcastExpandedProperties + * @property {import('./PodcastEpisode')[]} podcastEpisodes + * + * @typedef {Podcast & PodcastExpandedProperties} PodcastExpanded + */ + class Podcast extends Model { constructor(values, options) { super(values, options) From c0cb3a176f25f50f27a3cf4a30e8a5f0d402e50a Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 16 Jan 2024 17:19:45 -0600 Subject: [PATCH 0312/2145] Update:Hide audiobook tools for windows install, remove debian folder picker alert --- client/components/modals/item/tabs/Tools.vue | 27 ++++++++----------- .../modals/libraries/LazyFolderChooser.vue | 7 ----- client/strings/cs.json | 1 - client/strings/da.json | 1 - client/strings/de.json | 3 +-- client/strings/en-us.json | 1 - client/strings/es.json | 1 - client/strings/fr.json | 3 +-- client/strings/gu.json | 1 - client/strings/hi.json | 1 - client/strings/hr.json | 1 - client/strings/it.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/sv.json | 1 - client/strings/zh-cn.json | 1 - 19 files changed, 13 insertions(+), 42 deletions(-) diff --git a/client/components/modals/item/tabs/Tools.vue b/client/components/modals/item/tabs/Tools.vue index 5f2ca6b3..de19e04c 100644 --- a/client/components/modals/item/tabs/Tools.vue +++ b/client/components/modals/item/tabs/Tools.vue @@ -2,8 +2,11 @@ <div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6"> <p class="text-xl font-semibold mb-2">{{ $strings.HeaderAudiobookTools }}</p> + <!-- alert for windows install --> + <widgets-alert v-if="isWindowsInstall" type="warning" class="my-8 text-base">Not supported for the Windows install yet</widgets-alert> + <!-- Merge to m4b --> - <div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8"> + <div v-if="showM4bDownload && !isWindowsInstall" class="w-full border border-black-200 p-4 my-8"> <div class="flex flex-wrap items-center"> <div> <p class="text-lg">{{ $strings.LabelToolsMakeM4b }}</p> @@ -19,22 +22,8 @@ </div> </div> - <!-- Split to mp3 --> - <!-- <div v-if="showMp3Split" class="w-full border border-black-200 p-4 my-8"> - <div class="flex items-center"> - <div> - <p class="text-lg">{{ $strings.LabelToolsSplitM4b }}</p> - <p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsSplitM4bDescription }}</p> - </div> - <div class="flex-grow" /> - <div> - <ui-btn :disabled="true">{{ $strings.MessageNotYetImplemented }}</ui-btn> - </div> - </div> - </div> --> - <!-- Embed Metadata --> - <div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8"> + <div v-if="mediaTracks.length && !isWindowsInstall" class="w-full border border-black-200 p-4 my-8"> <div class="flex items-center"> <div> <p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p> @@ -122,6 +111,12 @@ export default { }, isEncodeTaskRunning() { return this.encodeTask && !this.encodeTask?.isFinished + }, + isWindowsInstall() { + return this.Source == 'windows' + }, + Source() { + return this.$store.state.Source } }, methods: { diff --git a/client/components/modals/libraries/LazyFolderChooser.vue b/client/components/modals/libraries/LazyFolderChooser.vue index 0254f760..74a81a97 100644 --- a/client/components/modals/libraries/LazyFolderChooser.vue +++ b/client/components/modals/libraries/LazyFolderChooser.vue @@ -35,7 +35,6 @@ <div v-else class="py-12 text-center max-w-sm mx-auto"> <p class="text-lg mb-2">{{ $strings.MessageNoFoldersAvailable }}</p> <p class="text-gray-300 mb-2">{{ $strings.NoteFolderPicker }}</p> - <p v-if="isDebian" class="text-red-400">{{ $strings.NoteFolderPickerDebian }}</p> </div> <div class="w-full py-2"> @@ -93,12 +92,6 @@ export default { ...d } }) - }, - isDebian() { - return this.Source == 'debian' - }, - Source() { - return this.$store.state.Source } }, methods: { diff --git a/client/strings/cs.json b/client/strings/cs.json index bac376d8..18e35553 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -668,7 +668,6 @@ "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.", diff --git a/client/strings/da.json b/client/strings/da.json index 3dd611d9..0d3cfafa 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -668,7 +668,6 @@ "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.", diff --git a/client/strings/de.json b/client/strings/de.json index 20da77c1..c92e9296 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann", "NoteChapterEditorTimes": "Hinweis: Die Anfangszeit des ersten Kapitels muss bei 0:00 beginnen und die Anfangszeit des letzten Kapitels darf die Dauer des Mediums nicht überschreiten.", "NoteFolderPicker": "Hinweis: Bereits zugeordnete Ordner werden nicht angezeigt.", - "NoteFolderPickerDebian": "Hinweis: Der Ordnerauswahldialog für die Debian-Installation ist nicht vollständig implementiert. Sie sollten den Pfad zu Ihrer Bibliothek direkt eingeben.", "NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.", "NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.", "NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.", @@ -750,4 +749,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 f69175fd..51efaef5 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "Root user is the only user that can have an empty password", "NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.", "NoteFolderPicker": "Note: folders already mapped will not be shown", - "NoteFolderPickerDebian": "Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.", "NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS", "NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.", "NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.", diff --git a/client/strings/es.json b/client/strings/es.json index cefeb8f8..f6edc557 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "El usuario Root es el único usuario que puede no tener una contraseña", "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.", diff --git a/client/strings/fr.json b/client/strings/fr.json index d894412c..6cf51544 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "seul l’utilisateur « root » peut utiliser un mot de passe vide", "NoteChapterEditorTimes": "Information : l’horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du livre audio.", "NoteFolderPicker": "Information : Les dossiers déjà surveillés ne sont pas affichés", - "NoteFolderPickerDebian": "Information : La sélection de dossier sur une installation debian n’est pas finalisée. Merci de renseigner le chemin complet vers votre bibliothèque manuellement.", "NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.", "NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.", "NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.", @@ -750,4 +749,4 @@ "ToastSocketFailedToConnect": "Échec de la connexion WebSocket", "ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur", "ToastUserDeleteSuccess": "Utilisateur supprimé" -} +} \ No newline at end of file diff --git a/client/strings/gu.json b/client/strings/gu.json index 24c874eb..9775ffad 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "Root user is the only user that can have an empty password", "NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.", "NoteFolderPicker": "Note: folders already mapped will not be shown", - "NoteFolderPickerDebian": "Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.", "NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS", "NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.", "NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.", diff --git a/client/strings/hi.json b/client/strings/hi.json index e7ec6155..205d674d 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "रूट user is the only user that can have an empty password", "NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.", "NoteFolderPicker": "Note: folders already mapped will not be shown", - "NoteFolderPickerDebian": "Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.", "NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS", "NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.", "NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.", diff --git a/client/strings/hr.json b/client/strings/hr.json index edefcf53..daa2479c 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "Root korisnik je jedini korisnik koji može imati praznu lozinku", "NoteChapterEditorTimes": "Bilješka: Prvo početno vrijeme poglavlja mora ostati na 0:00 i posljednje vrijeme poglavlja ne smije preći vrijeme trajanja ove audio knjige.", "NoteFolderPicker": "Bilješka: več mapirani folderi neće biti prikazani", - "NoteFolderPickerDebian": "Bilješka: Folder picker za debian instalaciju nije potpuno implementiran. Trebate unjeti direktnu putanju do biblioteke.", "NoteRSSFeedPodcastAppsHttps": "Upozorenje: Večina podcasta će trebati RSS feed URL koji koristi HTTPS", "NoteRSSFeedPodcastAppsPubDate": "Upozorenje: 1 ili više vaših epizoda nemaju datum objavljivanja. Neke podcast aplikacije zahtjevaju to.", "NoteUploaderFoldersWithMediaFiles": "Folderi sa media datotekama će biti tretirane kao odvojene stavke u biblioteki.", diff --git a/client/strings/it.json b/client/strings/it.json index 0860e83f..9aedfb22 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "L'utente root è l'unico utente che può avere una password vuota", "NoteChapterEditorTimes": "Nota: l'ora di inizio del primo capitolo deve rimanere alle 0:00 e l'ora di inizio dell'ultimo capitolo non può superare la durata di questo audiolibro.", "NoteFolderPicker": "Nota: le cartelle già mappate non verranno visualizzate", - "NoteFolderPickerDebian": "Nota: il selettore di cartelle per l'installazione di Debian non è completamente implementato. Dovresti inserire direttamente il percorso della tua libreria.", "NoteRSSFeedPodcastAppsHttps": "Avviso: la maggior parte delle app di podcast richiede che l'URL del feed RSS utilizzi HTTPS", "NoteRSSFeedPodcastAppsPubDate": "Avviso: 1 o più delle tue puntate non hanno una data di pubblicazione. Alcune app di podcast lo richiedono.", "NoteUploaderFoldersWithMediaFiles": "Le cartelle con file multimediali verranno gestite come elementi della libreria separati.", diff --git a/client/strings/lt.json b/client/strings/lt.json index 94067198..e1f6ec30 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "Tik root vartotojas gali turėti tuščią slaptažodį", "NoteChapterEditorTimes": "Pastaba: Pirmasis skyriaus pradžios laikas turi likti 0:00, o paskutinio skyriaus pradžios laikas negali viršyti šios garso knygos trukmės.", "NoteFolderPicker": "Pastaba: jau susieti aplankai nebus rodomi", - "NoteFolderPickerDebian": "Pastaba: Aplanko pasirinkimo įrankis „Debian“ sistemoje nėra visiškai įgyvendintas. Turėtumėte tiesiogiai įvesti kelią į savo biblioteką.", "NoteRSSFeedPodcastAppsHttps": "Įspėjimas: Dauguma tinklalaidžių programų reikalauja, kad RSS kanalo URL būtų naudojamas su HTTPS", "NoteRSSFeedPodcastAppsPubDate": "Įspėjimas: Vienas ar daugiau jūsų epizodų neturi publikavimo datos. Kai kurios tinklalaidžių programos to reikalauja.", "NoteUploaderFoldersWithMediaFiles": "Aplankai su medijos failais bus tvarkomi kaip atskiri bibliotekos elementai.", diff --git a/client/strings/nl.json b/client/strings/nl.json index 19a5c35a..3d1888ef 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "Root-gebruiker is de enige gebruiker die een leeg wachtwoord kan hebben", "NoteChapterEditorTimes": "Opmerking: Starttijd van het eerste hoofdstuk moet op 0:00 blijven en de starttijd van het laatste hoofdstuk mag niet de duur van het audioboek overschrijden.", "NoteFolderPicker": "Opmerking: Reeds gemapte mappen worden niet getoond", - "NoteFolderPickerDebian": "Opmerking: Mappenkiezer voor de debian installatie is niet volledig geimplementeerd. Je moet het pad naar je map zelf invoeren.", "NoteRSSFeedPodcastAppsHttps": "Waarschuwing: De meeste podcast-apps zullen eisen dat de RSS-feed URL HTTPS gebruikt", "NoteRSSFeedPodcastAppsPubDate": "Waarschuwing: 1 of meer van je afleveringen hebben geen Pub Datum. Sommige podcast-apps vereisen dit.", "NoteUploaderFoldersWithMediaFiles": "Mappen met mediabestanden zullen worden behandeld als aparte bibliotheekonderdelen.", diff --git a/client/strings/no.json b/client/strings/no.json index 37cbc7a8..49aceffe 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "Root-bruker er eneste bruker som kan ha tumt passord", "NoteChapterEditorTimes": "Notis: Første kapittel start tid må være 0:00 og siste kapittel start tid kan ikke overskride denne lydbokens lengde.", "NoteFolderPicker": "Notis: allerede funnet mapper vil ikke bli vist", - "NoteFolderPickerDebian": "Notis: Mappevelger for debian er ikke fullstendig implementert. Du burde skrive inn stien til biblioteket direkte.", "NoteRSSFeedPodcastAppsHttps": "Advarsel! De fleste podcast applikasjoner trenger RSS feed URL som bruker HTTPS", "NoteRSSFeedPodcastAppsPubDate": "Advarsel! 1 eller flere av episodene har ikke publikasjonsdato. Noen podcast applikasjoner trenger dette.", "NoteUploaderFoldersWithMediaFiles": "Mapper med mediefiler vil bli behandlet som separate bibliotekgjenstander.", diff --git a/client/strings/pl.json b/client/strings/pl.json index 86e29274..799d8c39 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "Tylko użytkownik root, może posiadać puste hasło", "NoteChapterEditorTimes": "Uwaga: Czas rozpoczęcia pierwszego rozdziału musi pozostać na poziomie 0:00, a czas rozpoczęcia ostatniego rozdziału nie może przekroczyć czasu trwania audiobooka.", "NoteFolderPicker": "Uwaga: dotychczas zmapowane foldery nie zostaną wyświetlone", - "NoteFolderPickerDebian": "Uwaga: Wybór folderu w instalcji opartej o system debian nie jest w pełni zaimplementowany. Powinieneś wprowadzić ścieżkę do swojej biblioteki bezpośrednio.", "NoteRSSFeedPodcastAppsHttps": "Ostrzeżenie: Większość aplikacji do obsługi podcastów wymaga, aby adres URL kanału RSS korzystał z protokołu HTTPS.", "NoteRSSFeedPodcastAppsPubDate": "Ostrzeżenie: 1 lub więcej odcinków nie ma daty publikacji. Niektóre aplikacje do słuchania podcastów tego wymagają.", "NoteUploaderFoldersWithMediaFiles": "Foldery z plikami multimedialnymi będą traktowane jako osobne elementy w bibliotece.", diff --git a/client/strings/ru.json b/client/strings/ru.json index 4d26c4aa..aa1cd7e5 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "Пользователь root — единственный пользователь, который может иметь пустой пароль", "NoteChapterEditorTimes": "Примечание: Время начала первой главы должно оставаться в 0:00, а время начала последней главы не может превышать продолжительность этой аудиокниги.", "NoteFolderPicker": "Примечание: папки, уже сопоставленные, не будут отображаться", - "NoteFolderPickerDebian": "Примечание: Выбор папок debian не реализован полностью. Необходимо ввести путь к библиотеке напрямую.", "NoteRSSFeedPodcastAppsHttps": "Предупреждение: Большинству приложений подкастов потребуется, чтобы URL-адрес RSS-канала использовал HTTPS", "NoteRSSFeedPodcastAppsPubDate": "Предупреждение: 1 или более эпизодов не имеют даты публикации. Некоторые приложения для подкастов требуют этого.", "NoteUploaderFoldersWithMediaFiles": "Папки с медиафайлами будут обрабатываться как отдельные элементы библиотеки.", diff --git a/client/strings/sv.json b/client/strings/sv.json index 1d71fce5..8832a0aa 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -668,7 +668,6 @@ "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.", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 05553c08..05ef483b 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -668,7 +668,6 @@ "NoteChangeRootPassword": "Root 是唯一可以拥有空密码的用户", "NoteChapterEditorTimes": "注意: 第一章开始时间必须保持在 0:00, 最后一章开始时间不能超过有声读物持续时间.", "NoteFolderPicker": "注意: 将不显示已映射的文件夹", - "NoteFolderPickerDebian": "注意: debian 安装的文件夹选择器尚未完全实现. 您应该直接输入媒体库的路径.", "NoteRSSFeedPodcastAppsHttps": "警告: 大多数播客应用程序都需要 RSS 源 URL 使用 HTTPS", "NoteRSSFeedPodcastAppsPubDate": "警告: 您的一集或多集没有发布日期. 一些播客应用程序要求这样做.", "NoteUploaderFoldersWithMediaFiles": "包含媒体文件的文件夹将作为单独的媒体库项目处理.", From 90f4833c9e0957f08799af15966d1909516b335e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 16 Jan 2024 17:26:50 -0600 Subject: [PATCH 0313/2145] Version bump v2.7.2 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 094eb3f7..a21c654e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.7.1", + "version": "2.7.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.7.1", + "version": "2.7.2", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 63d7978f..2195f320 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.7.1", + "version": "2.7.2", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index a54cc617..0e323ca3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.7.1", + "version": "2.7.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.7.1", + "version": "2.7.2", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index dca9710b..d7f38bcb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.7.1", + "version": "2.7.2", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From 7f350279fae1ea51fa736c32478f5c650e58fc4f Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 19 Jan 2024 17:54:41 -0600 Subject: [PATCH 0314/2145] Update to node20 - updates many dependencies - removes @nuxtjs/tailwindcss and postcss8 - pkg targets are using node18 until node20 targets are available --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 2 +- Dockerfile | 4 +- build/linuxpackager | 2 +- client/assets/tailwind.css | 3 + client/components/cards/LazyBookCard.vue | 2 +- client/nuxt.config.js | 5 +- client/package-lock.json | 26963 +++++---------------- client/package.json | 8 +- client/tailwind.config.js | 39 +- package-lock.json | 5793 +---- package.json | 4 +- readme.md | 2 +- 13 files changed, 7554 insertions(+), 25275 deletions(-) create mode 100644 client/assets/tailwind.css diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 968e97c4..ddd7c5f3 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster -ARG VARIANT=16 +ARG VARIANT=20 FROM mcr.microsoft.com/devcontainers/javascript-node:0-${VARIANT} as base # Setup the node environment diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1341b2c8..0213d517 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ // Append -bullseye or -buster to pin to an OS version. // Use -bullseye variants on local arm64/Apple Silicon. "args": { - "VARIANT": "16" + "VARIANT": "20" } }, "mounts": [ diff --git a/Dockerfile b/Dockerfile index 943fc567..97bb4732 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ### STAGE 0: Build client ### -FROM node:16-alpine AS build +FROM node:20-alpine AS build WORKDIR /client COPY /client /client RUN npm ci && npm cache clean --force @@ -7,7 +7,7 @@ RUN npm run generate ### STAGE 1: Build server ### FROM sandreas/tone:v0.1.5 AS tone -FROM node:16-alpine +FROM node:20-alpine ENV NODE_ENV=production diff --git a/build/linuxpackager b/build/linuxpackager index 903ee74f..a43d4ed1 100755 --- a/build/linuxpackager +++ b/build/linuxpackager @@ -48,7 +48,7 @@ Description: $DESCRIPTION" echo "$controlfile" > dist/debian/DEBIAN/control; # Package debian -pkg -t node16-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf . +pkg -t node18-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf . fakeroot dpkg-deb --build dist/debian diff --git a/client/assets/tailwind.css b/client/assets/tailwind.css new file mode 100644 index 00000000..bd6213e1 --- /dev/null +++ b/client/assets/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 04b3ce59..42b020e3 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -8,7 +8,7 @@ <!-- Alternative bookshelf title/author/sort --> <div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }"> <div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }"> - <ui-tooltip :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center"> + <ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center"> <p ref="displayTitle" class="truncate">{{ displayTitle }}</p> <widgets-explicit-indicator :explicit="isExplicit" /> </ui-tooltip> diff --git a/client/nuxt.config.js b/client/nuxt.config.js index 2494fa26..1f02bcaf 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -39,6 +39,7 @@ module.exports = { // Global CSS: https://go.nuxtjs.dev/config-css css: [ + '@/assets/tailwind.css', '@/assets/app.css' ], @@ -58,9 +59,7 @@ module.exports = { // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules buildModules: [ // https://go.nuxtjs.dev/tailwindcss - '@nuxtjs/tailwindcss', - '@nuxtjs/pwa', - '@nuxt/postcss8' + '@nuxtjs/pwa' ], // Modules: https://go.nuxtjs.dev/config-modules diff --git a/client/package-lock.json b/client/package-lock.json index a21c654e..b13fe9ee 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,7 +1,7 @@ { "name": "audiobookshelf-client", "version": "2.7.2", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -18,7 +18,7 @@ "epubjs": "^0.3.88", "hls.js": "^1.0.7", "libarchive.js": "^1.3.0", - "nuxt": "^2.15.8", + "nuxt": "^2.17.3", "nuxt-socket-io": "^1.1.18", "trix": "^1.3.1", "v-click-outside": "^3.1.2", @@ -26,20 +26,30 @@ "vuedraggable": "^2.24.3" }, "devDependencies": { - "@nuxt/postcss8": "^1.1.3", "@nuxtjs/pwa": "^3.3.5", - "@nuxtjs/tailwindcss": "^4.2.1", "autoprefixer": "^10.4.7", "postcss": "^8.3.6", - "tailwindcss": "^3.1.4" + "tailwindcss": "^3.4.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { @@ -47,44 +57,101 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.23.4", + "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==", + "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==", + "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==", + "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==" + }, + "node_modules/@babel/code-frame/node_modules/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==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", - "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz", - "integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", + "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.2", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-module-transforms": "^7.20.2", - "@babel/helpers": "^7.20.1", - "@babel/parser": "^7.20.2", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2", - "convert-source-map": "^1.7.0", + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -94,100 +161,98 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz", - "integrity": "sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dependencies": { - "@babel/types": "^7.20.2", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "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/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", - "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dependencies": { - "@babel/compat-data": "^7.20.0", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.2.tgz", - "integrity": "sha512-k22GoYRAHPYr9I+Gvy2ZQlAe5mGy8BqWst2wRt8cwIufWTxrsVshhIBvYNqC80N0GSFWTsqRVexOtfzlgOEDvA==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz", + "integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -196,155 +261,151 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz", - "integrity": "sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "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==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "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==", "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", - "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", "dependencies": { - "@babel/types": "^7.18.9" + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "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==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", - "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2" + "@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-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -354,111 +415,111 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz", - "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/traverse": "^7.19.1", - "@babel/types": "^7.19.0" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "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==", "dependencies": { - "@babel/types": "^7.20.2" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", "dependencies": { - "@babel/types": "^7.20.0" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "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==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "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==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz", - "integrity": "sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", - "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz", + "integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==", "dependencies": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.0" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -522,9 +583,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.3.tgz", - "integrity": "sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -533,11 +594,11 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -547,13 +608,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", - "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-proposal-optional-chaining": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -562,27 +623,26 @@ "@babel/core": "^7.13.0" } }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz", - "integrity": "sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==", + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-proposal-class-properties": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -594,92 +654,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", - "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.2.tgz", - "integrity": "sha512-nkBH96IBmgKnbHQ5gXFrcmez+Z9S2EIDKDQGp005ROqBigc88Tky4rzCnlP/lnlj245dCEQl4/YyV0V1kYh5dw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.23.7.tgz", + "integrity": "sha512-b1s5JyeMvqj7d9m9KhJNHKc18gEJiSyVzVX3bwbiPalQBQpuvfPh6lA9F7Kk/dWH0TIiXRpB9yicwijY6buPng==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.20.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/plugin-syntax-decorators": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", - "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-create-class-features-plugin": "^7.23.7", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-decorators": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -692,6 +674,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" @@ -703,61 +686,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz", - "integrity": "sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==", - "dependencies": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", - "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { @@ -771,6 +707,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -783,13 +720,14 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", - "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -799,21 +737,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.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", @@ -851,11 +774,11 @@ } }, "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz", - "integrity": "sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.23.3.tgz", + "integrity": "sha512-cf7Niq4/+/juY67E0PbgH0TDhLQ5J7zS8C/Q5FFx+DWyrRa9sUQdTXkjqKu8zGvuqr7vw1muKiukseihU+PJDA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -887,11 +810,11 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -900,6 +823,31 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", + "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-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==", + "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", @@ -912,11 +860,11 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1019,28 +967,60 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", - "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { "node": ">=6.9.0" }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz", + "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", - "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6" + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1050,11 +1030,11 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1064,11 +1044,11 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.2.tgz", - "integrity": "sha512-y5V15+04ry69OV2wULmwhEA6jwSWXO1TwAtIwiPXcvHcoOQUqpyMVd2bDsQJMW8AurjulIyUV8kDqtjSwHy1uQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1077,19 +1057,49 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.2.tgz", - "integrity": "sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g==", + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", + "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, "engines": { @@ -1100,11 +1110,12 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", - "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1114,11 +1125,11 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz", - "integrity": "sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1128,12 +1139,12 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1143,11 +1154,26 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1157,12 +1183,27 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1172,11 +1213,12 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", - "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1186,13 +1228,28 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1202,11 +1259,26 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { "node": ">=6.9.0" @@ -1216,11 +1288,11 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1230,12 +1302,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz", - "integrity": "sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", "dependencies": { - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1245,13 +1317,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz", - "integrity": "sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", "dependencies": { - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-simple-access": "^7.19.4" + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1261,14 +1333,14 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz", - "integrity": "sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", + "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-identifier": "^7.19.1" + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1278,12 +1350,12 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1293,12 +1365,12 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz", - "integrity": "sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1308,11 +1380,59 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "dependencies": { + "@babel/compat-data": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1322,12 +1442,43 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1337,11 +1488,43 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.3.tgz", - "integrity": "sha512-oZg/Fpx0YDrj13KsLyO8I/CX3Zdw7z0O9qOd95SqcoIzuqy/WTGWvePeHAnZCN54SfdyjHcb1S30gc8zlzlHcA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -1351,11 +1534,11 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1365,12 +1548,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", - "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "regenerator-transform": "^0.15.0" + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" }, "engines": { "node": ">=6.9.0" @@ -1380,11 +1563,11 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1394,16 +1577,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", - "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.7.tgz", + "integrity": "sha512-fa0hnfmiXc9fq/weK34MUV0drz2pOL/vfKWvN7Qw127hiUPabFCUMgAbYWcchRzMJit4o5ARsK/s+5h0249pLw==", "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.19.0", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "semver": "^6.3.0" + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.7", + "babel-plugin-polyfill-corejs3": "^0.8.7", + "babel-plugin-polyfill-regenerator": "^0.5.4", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1413,19 +1596,19 @@ } }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1435,12 +1618,12 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz", - "integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1450,11 +1633,11 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1464,11 +1647,11 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1478,11 +1661,11 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1492,11 +1675,26 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1506,12 +1704,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1520,38 +1718,42 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-env": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", - "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", "dependencies": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.20.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.8.tgz", + "integrity": "sha512-lFlpmkApLkEP6woIKprO6DO60RImpatTQKtz4sUcDjVcK8M8mQ4sZsuxaTMNOZf0sqAq/ReYW1ZBHnOQwKpLWA==", + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", @@ -1561,45 +1763,61 @@ "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.20.2", - "@babel/plugin-transform-classes": "^7.20.2", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.20.2", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.19.6", - "@babel/plugin-transform-modules-commonjs": "^7.19.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.6", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.20.1", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.7", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.8", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.3", + "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.7", + "babel-plugin-polyfill-corejs3": "^0.8.7", + "babel-plugin-polyfill-regenerator": "^0.5.4", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1608,67 +1826,81 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" + }, "node_modules/@babel/runtime": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz", - "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", "dependencies": { - "regenerator-runtime": "^0.13.10" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@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.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", - "debug": "^4.1.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@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.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1676,24 +1908,885 @@ } }, "node_modules/@babel/types": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz", - "integrity": "sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@csstools/convert-colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", - "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.7.tgz", + "integrity": "sha512-9J4aMRJ7A2WRjaRLvsMeWrL69FmEuijtiW1XlK/sG+V0UJiHVYUyvj9mY4WAXfU/hGIiGOgL8e0jJcRyaZTjDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "engines": { - "node": ">=4.0.0" + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-4.0.0.tgz", + "integrity": "sha512-wjyXB22/h2OvxAr3jldPB7R7kjTUEzopvjitS8jWtyd8fN6xJ8vy1HnHu0ZNfEkqpBJgQ76Q+sBDshWcMvTa/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-1.1.6.tgz", + "integrity": "sha512-YHPAuFg5iA4qZGzMzvrQwzkvJpesXXyIUyaONflQrjtHB+BcFFbgltJkIkb31dMGO4SE9iZFA4HYpdk7+hnYew==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-1.5.1.tgz", + "integrity": "sha512-x+SajGB2paGrTjPOUorGi8iCztF008YMKXTn+XzGVDBEIVJ/W1121pPerpneJYGOe1m6zWLPLnzOPaznmQxKFw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^4.0.0", + "@csstools/css-calc": "^1.1.6" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz", + "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^2.2.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz", + "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.7.tgz", + "integrity": "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.2.tgz", + "integrity": "sha512-PqM+jvg5T2tB4FHX+akrMGNWAygLupD4FNUjcv4PSvtVuWZ6ISxuo37m4jFGU7Jg3rCfloGzKd0+xfr5Ec3vZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/selector-specificity": "^3.0.1", + "postcss-selector-parser": "^6.0.13" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-3.0.9.tgz", + "integrity": "sha512-6Hbkw/4k73UH121l4LG+LNLKSvrfHqk3GHHH0A6/iFlD0xGmsWAr80Jd0VqXjfYbUTOGmJTOMMoxv3jvNxt1uw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^1.5.1", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "@csstools/postcss-progressive-custom-properties": "^3.0.3" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-function": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-2.0.9.tgz", + "integrity": "sha512-fs1SOWJ/44DQSsDeJP+rxAkP2MYkCg6K4ZB8qJwFku2EjurgCAPiPZJvC6w94T1hBBinJwuMfT9qvvvniXyVgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^1.5.1", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "@csstools/postcss-progressive-custom-properties": "^3.0.3" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-exponential-functions": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-1.0.3.tgz", + "integrity": "sha512-IfGtEg3eC4b8Nd/kPgO3SxgKb33YwhHVsL0eJ3UYihx6fzzAiZwNbWmVW9MZTQjZ5GacgKxa4iAHikGvpwuIjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-calc": "^1.1.6", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-3.0.1.tgz", + "integrity": "sha512-D1lcG2sfotTq6yBEOMV3myFxJLT10F3DLYZJMbiny5YToqzHWodZen8WId3UTimm0mEHitXqAUNL5jdd6RzVdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-1.0.2.tgz", + "integrity": "sha512-zf9KHGM2PTuJEm4ZYg4DTmzCir38EbZBzlMPMbA4jbhLDqXHkqwnQ+Z5+UNrU8y6seVu5B4vzZmZarTFQwe+Ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^1.5.1", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-4.0.9.tgz", + "integrity": "sha512-PSqR6QH7h3ggOl8TsoH73kbwYTKVQjAJauGg6nDKwaGfi5IL5StV//ehrv1C7HuPsHixMTc9YoAuuv1ocT20EQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^1.5.1", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "@csstools/postcss-progressive-custom-properties": "^3.0.3" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-3.0.8.tgz", + "integrity": "sha512-CRQEG372Hivmt17rm/Ho22hBQI9K/a6grzGQ21Zwc7dyspmyG0ibmPIW8hn15vJmXqWGeNq7S+L2b8/OrU7O5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^1.5.1", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-3.0.3.tgz", + "integrity": "sha512-MpcmIL0/uMm/cFWh5V/9nbKKJ7jRr2qTYW5Q6zoE6HZ6uzOBJr2KRERv5/x8xzEBQ1MthDT7iP1EBp9luSQy7g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^3.0.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-1.0.1.tgz", + "integrity": "sha512-wtb+IbUIrIf8CrN6MLQuFR7nlU5C7PwuebfeEXfjthUha1+XZj2RVi+5k/lukToA24sZkYAiSJfHM8uG/UZIdg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-4.0.4.tgz", + "integrity": "sha512-vTVO/uZixpTVAOQt3qZRUFJ/K1L03OfNkeJ8sFNDVNdVy/zW0h1L5WT7HIPMDUkvSrxQkFaCCybTZkUP7UESlQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/selector-specificity": "^3.0.1", + "postcss-selector-parser": "^6.0.13" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-2.0.1.tgz", + "integrity": "sha512-SsrWUNaXKr+e/Uo4R/uIsqJYt3DaggIh/jyZdhy/q8fECoJSKsSMr7nObSLdvoULB69Zb6Bs+sefEIoMG/YfOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-1.0.1.tgz", + "integrity": "sha512-Kl4lAbMg0iyztEzDhZuQw8Sj9r2uqFDcU1IPl+AAt2nue8K/f1i7ElvKtXkjhIAmKiy5h2EY8Gt/Cqg0pYFDCw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-1.0.1.tgz", + "integrity": "sha512-+kHamNxAnX8ojPCtV8WPcUP3XcqMFBSDuBuvT6MHgq7oX4IQxLIXKx64t7g9LiuJzE7vd06Q9qUYR6bh4YnGpQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-2.0.1.tgz", + "integrity": "sha512-W5Gtwz7oIuFcKa5SmBjQ2uxr8ZoL7M2bkoIf0T1WeNqljMkBrfw1DDA8/J83k57NQ1kcweJEjkJ04pUkmyee3A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-2.0.5.tgz", + "integrity": "sha512-2fjSamKN635DSW6fEoyNd2Bkpv3FVblUpgk5cpghIgPW1aDHZE2SYfZK5xQALvjMYZVjfqsD5EbXA7uDVBQVQA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-tokenizer": "^2.2.3" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-1.1.2.tgz", + "integrity": "sha512-7qTRTJxW96u2yiEaTep1+8nto1O/rEDacewKqH+Riq5E6EsHTOmGHxkB4Se5Ic5xgDC4I05lLZxzzxnlnSypxA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-calc": "^1.1.6", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "@csstools/media-query-list-parser": "^2.1.7" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-2.0.5.tgz", + "integrity": "sha512-XHMPasWYPWa9XaUHXU6Iq0RLfoAI+nvGTPj51hOizNsHaAyFiq2SL4JvF1DU8lM6B70+HVzKM09Isbyrr755Bw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "@csstools/media-query-list-parser": "^2.1.7" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-3.0.1.tgz", + "integrity": "sha512-bwwababZpWRm0ByHaWBxTsDGTMhZKmtUNl3Wt0Eom8AY7ORgXx5qF9SSk1vEFrCi+HOfJT6M6W5KPgzXuQNRwQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-3.0.2.tgz", + "integrity": "sha512-fCapyyT/dUdyPtrelQSIV+d5HqtTgnNP/BEG9IuhgXHt93Wc4CfC1bQ55GzKAjWrZbgakMQ7MLfCXEf3rlZJOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-3.0.9.tgz", + "integrity": "sha512-l639gpcBfL3ogJe+og1M5FixQn8iGX8+29V7VtTSCUB37VzpzOC05URfde7INIdiJT65DkHzgdJ64/QeYggU8A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^1.5.1", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "@csstools/postcss-progressive-custom-properties": "^3.0.3" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-3.0.3.tgz", + "integrity": "sha512-WipTVh6JTMQfeIrzDV4wEPsV9NTzMK2jwXxyH6CGBktuWdivHnkioP/smp1x/0QDPQyx7NTS14RB+GV3zZZYEw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-2.0.9.tgz", + "integrity": "sha512-2UoaRd2iIuzUGtYgteN5fJ0s+OfCiV7PvCnw8MCh3om8+SeVinfG8D5sqBOvImxFVfrp6k60XF5RFlH6oc//fg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^1.5.1", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "@csstools/postcss-progressive-custom-properties": "^3.0.3" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-3.0.1.tgz", + "integrity": "sha512-3ZFonK2gfgqg29gUJ2w7xVw2wFJ1eNWVDONjbzGkm73gJHVCYK5fnCqlLr+N+KbEfv2XbWAO0AaOJCFB6Fer6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.0.13" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-3.0.4.tgz", + "integrity": "sha512-gyNQ2YaOVXPqLR737XtReRPVu7DGKBr9JBDLoiH1T+N1ggV3r4HotRCOC1l6rxVC0zOuU1KiOzUn9Z5W838/rg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-calc": "^1.1.6", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-3.0.4.tgz", + "integrity": "sha512-yUZmbnUemgQmja7SpOZeU45+P49wNEgQguRdyTktFkZsHf7Gof+ZIYfvF6Cm+LsU1PwSupy4yUeEKKjX5+k6cQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^4.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-3.0.4.tgz", + "integrity": "sha512-qj4Cxth6c38iNYzfJJWAxt8jsLrZaMVmbfGDDLOlI2YJeZoC3A5Su6/Kr7oXaPFRuspUu+4EQHngOktqVHWfVg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-calc": "^1.1.6", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-3.0.1.tgz", + "integrity": "sha512-dbDnZ2ja2U8mbPP0Hvmt2RMEGBiF1H7oY6HYSpjteXJGihYwgxgTr6KRbbJ/V6c+4wd51M+9980qG4gKVn5ttg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.1.tgz", + "integrity": "sha512-NPljRHkq4a14YzZ3YD406uaxh7s0g6eAq3L9aLOWywoqe8PkYamAvtsh7KNX6c++ihDrJ0RiU+/z7rGnhlZ5ww==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.13" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "engines": { + "node": ">=10.0.0" } }, "node_modules/@gar/promisify": { @@ -1701,22 +2794,119 @@ "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==" }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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==", + "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.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "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==", "engines": { "node": ">=6.0.0" } @@ -1730,80 +2920,26 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" } }, - "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "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/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@koa/router": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-9.4.0.tgz", - "integrity": "sha512-dOOXgzqaDoHu5qqMEPLKEgLz5CeIA7q8+1W62mCvFVCOqeC71UoTGJ4u1xUSOpIl2J1x2pqrNULkFteUeZW3/A==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "http-errors": "^1.7.3", - "koa-compose": "^4.1.0", - "methods": "^1.1.2", - "path-to-regexp": "^6.1.0" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/@koa/router/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@koa/router/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@nodelib/fs.scandir": { @@ -1860,6 +2996,45 @@ "node": ">=10" } }, + "node_modules/@npmcli/move-file/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@npmcli/move-file/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/move-file/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@npmcli/move-file/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -1886,95 +3061,140 @@ } }, "node_modules/@nuxt/babel-preset-app": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/babel-preset-app/-/babel-preset-app-2.15.8.tgz", - "integrity": "sha512-z23bY5P7dLTmIbk0ZZ95mcEXIEER/mQCOqEp2vxnzG2nurks+vq6tNcUAXqME1Wl6aXWTXlqky5plBe7RQHzhQ==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/babel-preset-app/-/babel-preset-app-2.17.3.tgz", + "integrity": "sha512-KkmGEKZN2Yvo9XWC2TAJ3e3WMFQTmGGdhJy9Lv/3gW0PCUVvI5e+M+P3VF494BLKWmc4xYXaVu7cGtAUE13vMQ==", "dependencies": { - "@babel/compat-data": "^7.14.0", - "@babel/core": "^7.14.0", - "@babel/helper-compilation-targets": "^7.13.16", - "@babel/helper-module-imports": "^7.13.12", - "@babel/plugin-proposal-class-properties": "^7.13.0", - "@babel/plugin-proposal-decorators": "^7.13.15", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", - "@babel/plugin-proposal-optional-chaining": "^7.13.12", - "@babel/plugin-proposal-private-methods": "^7.13.0", - "@babel/plugin-transform-runtime": "^7.13.15", - "@babel/preset-env": "^7.14.1", - "@babel/runtime": "^7.14.0", - "@vue/babel-preset-jsx": "^1.2.4", - "core-js": "^2.6.5", - "core-js-compat": "^3.12.1", - "regenerator-runtime": "^0.13.7" + "@babel/compat-data": "^7.23.5", + "@babel/core": "^7.23.7", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-imports": "^7.22.15", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-decorators": "^7.23.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@babel/plugin-transform-runtime": "^7.23.7", + "@babel/preset-env": "^7.23.8", + "@babel/runtime": "^7.23.8", + "@vue/babel-preset-jsx": "^1.4.0", + "core-js": "^3.35.0", + "core-js-compat": "^3.35.0", + "regenerator-runtime": "^0.14.1" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" } }, - "node_modules/@nuxt/babel-preset-app/node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true - }, "node_modules/@nuxt/builder": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/builder/-/builder-2.15.8.tgz", - "integrity": "sha512-WVhN874LFMdgRiJqpxmeKI+vh5lhCUBVOyR9PhL1m1V/GV3fb+Dqc1BKS6XgayrWAWavPLveCJmQ/FID0puOfQ==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/builder/-/builder-2.17.3.tgz", + "integrity": "sha512-qcByuB5+Sy9xHtHT6yxsX01fT4ZMIP1bqVYqtkuwuSnbtFLc6GW2qNpzWkcxSBCzhIp2hTuulOEV6p5FpQVPLg==", "dependencies": { - "@nuxt/devalue": "^1.2.5", - "@nuxt/utils": "2.15.8", - "@nuxt/vue-app": "2.15.8", - "@nuxt/webpack": "2.15.8", - "chalk": "^4.1.1", - "chokidar": "^3.5.1", - "consola": "^2.15.3", - "fs-extra": "^9.1.0", - "glob": "^7.1.7", + "@nuxt/devalue": "^2.0.2", + "@nuxt/utils": "2.17.3", + "@nuxt/vue-app": "2.17.3", + "@nuxt/webpack": "2.17.3", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "consola": "^3.2.3", + "fs-extra": "^10.1.0", + "glob": "^8.1.0", "hash-sum": "^2.0.0", - "ignore": "^5.1.8", + "ignore": "^5.3.0", "lodash": "^4.17.21", "pify": "^5.0.0", - "serialize-javascript": "^5.0.1", + "serialize-javascript": "^6.0.2", "upath": "^2.0.1" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/builder/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/@nuxt/cli": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/cli/-/cli-2.15.8.tgz", - "integrity": "sha512-KcGIILW/dAjBKea1DHsuLCG1sNzhzETShwT23DhXWO304qL8ljf4ndYKzn2RenzauGRGz7MREta80CbJCkLSHw==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/cli/-/cli-2.17.3.tgz", + "integrity": "sha512-UwQbb/B/b7/N1vlorBgiW2T41Hc26+04oyze2cx1dErkDNLgjZpjcTu8cWnfSyAeRUeG2r6p8K0SPimCITNZJg==", "dependencies": { - "@nuxt/config": "2.15.8", - "@nuxt/utils": "2.15.8", - "boxen": "^5.0.1", - "chalk": "^4.1.1", + "@nuxt/config": "2.17.3", + "@nuxt/utils": "2.17.3", + "boxen": "^5.1.2", + "chalk": "^4.1.2", "compression": "^1.7.4", "connect": "^3.7.0", - "consola": "^2.15.3", - "crc": "^3.8.0", - "defu": "^4.0.1", - "destr": "^1.1.0", - "execa": "^5.0.0", + "consola": "^3.2.3", + "crc": "^4.3.2", + "defu": "^6.1.4", + "destr": "^2.0.2", + "execa": "^5.1.1", "exit": "^0.1.2", - "fs-extra": "^9.1.0", - "globby": "^11.0.3", - "hable": "^3.0.0", + "fs-extra": "^10.1.0", + "globby": "^11.0.4", + "hookable": "^4.4.1", "lodash": "^4.17.21", - "minimist": "^1.2.5", + "minimist": "^1.2.8", "opener": "1.5.2", "pretty-bytes": "^5.6.0", - "semver": "^7.3.5", - "serve-static": "^1.14.1", - "std-env": "^2.3.0", + "semver": "^7.5.4", + "serve-static": "^1.15.0", + "std-env": "^3.7.0", "upath": "^2.0.1", "wrap-ansi": "^7.0.0" }, "bin": { "nuxt-cli": "bin/nuxt-cli.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/cli/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" } }, "node_modules/@nuxt/cli/node_modules/defu": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-4.0.1.tgz", - "integrity": "sha512-lC+G0KvvWRbisQa50+iFelm3/eMmwo4IlBmfASOVlw9MZpHHyQeVsZxc5j23+TQy5ydgEoTVSrWl7ptou1kzJQ==" + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + }, + "node_modules/@nuxt/cli/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } }, "node_modules/@nuxt/components": { "version": "2.2.1", @@ -1994,62 +3214,133 @@ "consola": "*" } }, - "node_modules/@nuxt/config": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/config/-/config-2.15.8.tgz", - "integrity": "sha512-KMQbjmUf9RVHeTZEf7zcuFnh03XKZioYhok6GOCY+leu3g5n/UhyPvLnTsgTfsLWohqoRoOm94u4A+tNYwn9VQ==", + "node_modules/@nuxt/components/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dependencies": { - "@nuxt/utils": "2.15.8", - "consola": "^2.15.3", - "defu": "^4.0.1", - "destr": "^1.1.0", - "dotenv": "^9.0.2", + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@nuxt/components/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nuxt/components/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@nuxt/config": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/config/-/config-2.17.3.tgz", + "integrity": "sha512-msHFykkeG2wPB8oP369Gha5n1O2RI57vLxSJcAnCrg6vrETfc6DadCsRA1o7v8Z9TjfVyU3leYy9c4F+elwFYg==", + "dependencies": { + "@nuxt/utils": "2.17.3", + "consola": "^3.2.3", + "defu": "^6.1.4", + "destr": "^2.0.2", + "dotenv": "^16.3.1", "lodash": "^4.17.21", - "rc9": "^1.2.0", - "std-env": "^2.3.0", - "ufo": "^0.7.4" + "rc9": "^2.1.1", + "std-env": "^3.7.0", + "ufo": "^1.3.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/config/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" } }, "node_modules/@nuxt/config/node_modules/defu": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-4.0.1.tgz", - "integrity": "sha512-lC+G0KvvWRbisQa50+iFelm3/eMmwo4IlBmfASOVlw9MZpHHyQeVsZxc5j23+TQy5ydgEoTVSrWl7ptou1kzJQ==" + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" }, "node_modules/@nuxt/core": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/core/-/core-2.15.8.tgz", - "integrity": "sha512-31pipWRvwHiyB5VDqffgSO7JtmHxyzgshIzuZzSinxMbVmK3BKsOwacD/51oEyELgrPlUgLqcY9dg+RURgmHGQ==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/core/-/core-2.17.3.tgz", + "integrity": "sha512-DAyxn49UUjmEyMImaPTjGpV7EccbbqZX14j46fWC7hNR5NkLPBMHFgYj+tsetYK5LMPcKUz1zztRoFX68SMxyw==", "dependencies": { - "@nuxt/config": "2.15.8", - "@nuxt/server": "2.15.8", - "@nuxt/utils": "2.15.8", - "consola": "^2.15.3", - "fs-extra": "^9.1.0", - "hable": "^3.0.0", + "@nuxt/config": "2.17.3", + "@nuxt/server": "2.17.3", + "@nuxt/utils": "2.17.3", + "consola": "^3.2.3", + "fs-extra": "^10.1.0", "hash-sum": "^2.0.0", + "hookable": "^4.4.1", "lodash": "^4.17.21" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/core/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/core/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/@nuxt/devalue": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-1.2.5.tgz", - "integrity": "sha512-Tg86C7tqzvZtZli2BQVqgzZN136mZDTgauvJXagglKkP2xt5Kw3NUIiJyjX0Ww/IZy2xVmD0LN+CEPpij4dB2g==", - "dependencies": { - "consola": "^2.9.0" - } + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz", + "integrity": "sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==" }, "node_modules/@nuxt/friendly-errors-webpack-plugin": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@nuxt/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-2.5.2.tgz", - "integrity": "sha512-LLc+90lnxVbpKkMqk5z1EWpXoODhc6gRkqqXJCInJwF5xabHAE7biFvbULfvTRmtaTzAaP8IV4HQDLUgeAUTTw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@nuxt/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-2.6.0.tgz", + "integrity": "sha512-3IZj6MXbzlvUxDncAxgBMLQwGPY/JlNhy2i+AGyOHCAReR5HcBxYjVRBvyaKM9R3s5k4OODYKeHAbrToZH/47w==", "dependencies": { - "chalk": "^2.3.2", - "consola": "^2.6.0", - "error-stack-parser": "^2.0.0", + "chalk": "^2.4.2", + "consola": "^3.2.3", + "error-stack-parser": "^2.1.4", "string-width": "^4.2.3" }, "engines": { - "node": ">=8.0.0", + "node": ">=14.18.0", "npm": ">=5.0.0" }, "peerDependencies": { @@ -2093,6 +3384,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/@nuxt/friendly-errors-webpack-plugin/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/@nuxt/friendly-errors-webpack-plugin/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2113,25 +3412,49 @@ } }, "node_modules/@nuxt/generator": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/generator/-/generator-2.15.8.tgz", - "integrity": "sha512-hreLdYbBIe3SWcP8LsMG7OlDTx2ZVucX8+f8Vrjft3Q4r8iCwLMYC1s1N5etxeHAZfS2kZiLmF92iscOdfbgMQ==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/generator/-/generator-2.17.3.tgz", + "integrity": "sha512-m/fnzH+1RvdpDdQODUxjXlMkJzLVuOwk9AOGYZz2YaAP34nxwjxClvgIAT6IQqvq6uBZHex0Zr07N3mwEE06NA==", "dependencies": { - "@nuxt/utils": "2.15.8", - "chalk": "^4.1.1", - "consola": "^2.15.3", - "defu": "^4.0.1", + "@nuxt/utils": "2.17.3", + "chalk": "^4.1.2", + "consola": "^3.2.3", + "defu": "^6.1.4", "devalue": "^2.0.1", - "fs-extra": "^9.1.0", + "fs-extra": "^10.1.0", "html-minifier": "^4.0.0", - "node-html-parser": "^3.2.0", - "ufo": "^0.7.4" + "node-html-parser": "^6.1.12", + "ufo": "^1.3.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/generator/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" } }, "node_modules/@nuxt/generator/node_modules/defu": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-4.0.1.tgz", - "integrity": "sha512-lC+G0KvvWRbisQa50+iFelm3/eMmwo4IlBmfASOVlw9MZpHHyQeVsZxc5j23+TQy5ydgEoTVSrWl7ptou1kzJQ==" + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + }, + "node_modules/@nuxt/generator/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } }, "node_modules/@nuxt/loading-screen": { "version": "2.0.4", @@ -2151,13 +3474,13 @@ "integrity": "sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==" }, "node_modules/@nuxt/opencollective": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.3.3.tgz", - "integrity": "sha512-6IKCd+gP0HliixqZT/p8nW3tucD6Sv/u/eR2A9X4rxT/6hXlMzA4GZQzq4d2qnBAwSwGpmKyzkyTjNjrhaA25A==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.0.tgz", + "integrity": "sha512-uUsxOcO2lFeotV+BGOwNLeau+U17mhpaCRhE7v8nJLdWJ2iErQXadl28HaHe6btuT8RD0LDSpvwCiKrHznDxUA==", "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.0", - "node-fetch": "^2.6.7" + "chalk": "^4.1.2", + "consola": "^3.2.3", + "node-fetch-native": "^1.4.1" }, "bin": { "opencollective": "bin/opencollective.js" @@ -2167,77 +3490,109 @@ "npm": ">=5.0.0" } }, - "node_modules/@nuxt/postcss8": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nuxt/postcss8/-/postcss8-1.1.3.tgz", - "integrity": "sha512-CdHtErhvQwueNZPBOmlAAKrNCK7aIpZDYhtS7TzXlSgPHHox1g3cSlf+Ke9oB/8t4mNNjdB+prclme2ibuCOEA==", - "dev": true, - "dependencies": { - "autoprefixer": "^10.2.5", - "css-loader": "^5.0.0", - "defu": "^3.2.2", - "postcss": "^8.1.10", - "postcss-import": "^13.0.0", - "postcss-loader": "^4.1.0", - "postcss-url": "^10.1.1", - "semver": "^7.3.4" + "node_modules/@nuxt/opencollective/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" } }, "node_modules/@nuxt/server": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/server/-/server-2.15.8.tgz", - "integrity": "sha512-E4EtXudxtWQBUHMHOxFwm5DlPOkJbW+iF1+zc0dGmXLscep1KWPrlP+4nrpZj8/UKzpupamE8ZTS9I4IbnExVA==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/server/-/server-2.17.3.tgz", + "integrity": "sha512-+HxDxni7nAHZdtBl1ptug6lHVio/aJn3o8ZkoHJjYuQ52dtJgEFsQs8EpDbKJDFYyL/u0TXEUPACrXbkOh9J8Q==", "dependencies": { - "@nuxt/utils": "2.15.8", - "@nuxt/vue-renderer": "2.15.8", + "@nuxt/utils": "2.17.3", + "@nuxt/vue-renderer": "2.17.3", "@nuxtjs/youch": "^4.2.3", "compression": "^1.7.4", "connect": "^3.7.0", - "consola": "^2.15.3", + "consola": "^3.2.3", "etag": "^1.8.1", "fresh": "^0.5.2", - "fs-extra": "^9.1.0", - "ip": "^1.1.5", - "launch-editor-middleware": "^2.2.1", + "fs-extra": "^10.1.0", + "ip": "^1.1.8", + "launch-editor-middleware": "^2.6.1", "on-headers": "^1.0.2", "pify": "^5.0.0", - "serve-placeholder": "^1.2.3", - "serve-static": "^1.14.1", + "serve-placeholder": "^2.0.1", + "serve-static": "^1.15.0", "server-destroy": "^1.0.1", - "ufo": "^0.7.4" + "ufo": "^1.3.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/server/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/server/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/@nuxt/telemetry": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-1.3.9.tgz", - "integrity": "sha512-QR+UjZ1VXBsLwNjxe5cFqy5yhU+0x13/+dyjSLfC2vwRRQLPlCCPIAhtB1pvnE6Qxn7hh8psxRYu/efWS7laDA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-1.5.0.tgz", + "integrity": "sha512-MhxiiYCFe0MayN2TvmpcsCV66zBePtrSVkFLJHwTFuneQ5Qma5x0NmCwdov7O4NSuTfgSZels9qPJh0zy0Kc4g==", "dependencies": { "arg": "^5.0.2", "chalk": "^4.1.1", - "ci-info": "^3.6.1", - "consola": "^2.15.3", + "ci-info": "^3.7.1", + "consola": "^3.2.3", "create-require": "^1.1.1", - "defu": "^5.0.0", - "destr": "^1.1.0", + "defu": "^6.1.3", + "destr": "^2.0.2", "dotenv": "^9.0.2", "fs-extra": "^8.1.0", - "git-url-parse": "^13.1.0", + "git-url-parse": "^13.1.1", "inquirer": "^7.3.3", - "jiti": "^1.9.2", + "jiti": "^1.21.0", "nanoid": "^3.1.23", "node-fetch": "^2.6.1", "parse-git-config": "^3.0.0", - "rc9": "^1.2.0", - "std-env": "^2.3.0" + "rc9": "^2.1.1", + "std-env": "^3.5.0" }, "bin": { "nuxt-telemetry": "bin/nuxt-telemetry.js" } }, + "node_modules/@nuxt/telemetry/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/@nuxt/telemetry/node_modules/defu": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-5.0.1.tgz", - "integrity": "sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==" + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + }, + "node_modules/@nuxt/telemetry/node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "engines": { + "node": ">=10" + } }, "node_modules/@nuxt/telemetry/node_modules/fs-extra": { "version": "8.1.0", @@ -2269,376 +3624,235 @@ } }, "node_modules/@nuxt/types": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/types/-/types-2.15.8.tgz", - "integrity": "sha512-zBAG5Fy+SIaZIerOVF1vxy1zz16ZK07QSbsuQAjdtEFlvr+vKK+0AqCv8r8DBY5IVqdMIaw5FgNUz5py0xWdPg==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/types/-/types-2.17.3.tgz", + "integrity": "sha512-IXM+DwDrBj96v2O+oQrqA1vhQMVnBBcU7lTb+Xhnl6StL2PU6hxcx2czUWS8p2K6B6xvOHu+Sda7rCOKP60j2g==", "dependencies": { - "@types/autoprefixer": "9.7.2", - "@types/babel__core": "7.1.14", - "@types/compression": "1.7.0", - "@types/connect": "3.4.34", - "@types/etag": "1.8.0", - "@types/file-loader": "5.0.0", - "@types/html-minifier": "4.0.0", - "@types/less": "3.0.2", - "@types/node": "12.20.12", - "@types/optimize-css-assets-webpack-plugin": "5.0.3", - "@types/pug": "2.0.4", - "@types/sass-loader": "8.0.1", - "@types/serve-static": "1.13.9", + "@types/babel__core": "7.20.5", + "@types/compression": "1.7.5", + "@types/connect": "3.4.38", + "@types/etag": "1.8.3", + "@types/file-loader": "5.0.4", + "@types/html-minifier": "4.0.5", + "@types/less": "3.0.6", + "@types/node": "^16", + "@types/optimize-css-assets-webpack-plugin": "5.0.8", + "@types/pug": "2.0.10", + "@types/serve-static": "1.15.5", "@types/terser-webpack-plugin": "4.2.1", - "@types/webpack": "4.41.28", - "@types/webpack-bundle-analyzer": "3.9.3", - "@types/webpack-dev-middleware": "4.1.2", - "@types/webpack-hot-middleware": "2.25.4", - "sass-loader": "10.1.1" + "@types/webpack": "^4.41.38", + "@types/webpack-bundle-analyzer": "3.9.5", + "@types/webpack-hot-middleware": "2.25.5" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/@nuxt/types/node_modules/@types/node": { + "version": "16.18.72", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.72.tgz", + "integrity": "sha512-Kck1Du/zQyLbq5YlBKCtrUlyyP02lYjREjKKYImtf6MZgXrLoRVjexMv0wxiDzIJPnk86i+HrvGNyI03qoewEg==" + }, "node_modules/@nuxt/utils": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/utils/-/utils-2.15.8.tgz", - "integrity": "sha512-e0VBarUbPiQ4ZO1T58puoFIuXme7L5gk1QfwyxOONlp2ryE7aRyZ8X/mryuOiIeyP64c4nwSUtN7q9EUWRb7Lg==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/utils/-/utils-2.17.3.tgz", + "integrity": "sha512-/ZdjQY+U3I6X+IiRaHX2zA9l/cgN9GD8YIYuvf2obo5u1cLHin0MNj2dwb4P2iYvygAppb8nmcEsVzG4bppoEA==", "dependencies": { - "consola": "^2.15.3", + "consola": "^3.2.3", "create-require": "^1.1.1", - "fs-extra": "^9.1.0", + "fs-extra": "^10.1.0", "hash-sum": "^2.0.0", - "jiti": "^1.9.2", + "jiti": "^1.21.0", "lodash": "^4.17.21", "proper-lockfile": "^4.1.2", - "semver": "^7.3.5", - "serialize-javascript": "^5.0.1", - "signal-exit": "^3.0.3", - "ua-parser-js": "^0.7.28", - "ufo": "^0.7.4" + "semver": "^7.5.4", + "serialize-javascript": "^6.0.2", + "signal-exit": "^4.1.0", + "ua-parser-js": "^1.0.37", + "ufo": "^1.3.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/utils/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/utils/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@nuxt/utils/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@nuxt/vue-app": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/vue-app/-/vue-app-2.15.8.tgz", - "integrity": "sha512-FJf9FSMPsWT3BqkS37zEuPTxLKzSg2EIwp1sP8Eou25eE08qxRfe2PwTVA8HnXUPNdpz2uk/T9DlNw+JraiFRQ==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/vue-app/-/vue-app-2.17.3.tgz", + "integrity": "sha512-MgB5TKTrZwgVaccMS9YKjNerlXsjnouEfe9Eo4ChVyDybMTy6apjN6QTg+YC/J/kzrsIxrFTbYnh30dAzuZdMw==", "dependencies": { - "node-fetch": "^2.6.1", - "ufo": "^0.7.4", - "unfetch": "^4.2.0", - "vue": "^2.6.12", - "vue-client-only": "^2.0.0", + "node-fetch-native": "^1.6.1", + "ufo": "^1.3.2", + "unfetch": "^5.0.0", + "vue": "^2.7.16", + "vue-client-only": "^2.1.0", "vue-meta": "^2.4.0", "vue-no-ssr": "^1.1.1", - "vue-router": "^3.5.1", - "vue-template-compiler": "^2.6.12", + "vue-router": "^3.6.5", + "vue-template-compiler": "^2.7.16", "vuex": "^3.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" } }, "node_modules/@nuxt/vue-renderer": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/vue-renderer/-/vue-renderer-2.15.8.tgz", - "integrity": "sha512-54I/k+4G6axP9XVYYdtH6M1S6T49OIkarpF6/yIJj0yi3S/2tdJ9eUyfoLZ9EbquZFDDRHBxSswTtr2l/eakPw==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/vue-renderer/-/vue-renderer-2.17.3.tgz", + "integrity": "sha512-rSSOdta3vh47FEP8W4d+tdvJMAqejGzgQojJcruuoe+vkbo2zovFFWyISZKMFw7SCVnm0wANAwETJHpb6a3Y6Q==", "dependencies": { - "@nuxt/devalue": "^1.2.5", - "@nuxt/utils": "2.15.8", - "consola": "^2.15.3", - "defu": "^4.0.1", - "fs-extra": "^9.1.0", + "@nuxt/devalue": "^2.0.2", + "@nuxt/utils": "2.17.3", + "consola": "^3.2.3", + "defu": "^6.1.4", + "fs-extra": "^10.1.0", "lodash": "^4.17.21", "lru-cache": "^5.1.1", - "ufo": "^0.7.4", - "vue": "^2.6.12", + "ufo": "^1.3.2", + "vue": "^2.7.16", "vue-meta": "^2.4.0", - "vue-server-renderer": "^2.6.12" + "vue-server-renderer": "^2.7.16" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/vue-renderer/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" } }, "node_modules/@nuxt/vue-renderer/node_modules/defu": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-4.0.1.tgz", - "integrity": "sha512-lC+G0KvvWRbisQa50+iFelm3/eMmwo4IlBmfASOVlw9MZpHHyQeVsZxc5j23+TQy5ydgEoTVSrWl7ptou1kzJQ==" + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + }, + "node_modules/@nuxt/vue-renderer/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } }, "node_modules/@nuxt/webpack": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/webpack/-/webpack-2.15.8.tgz", - "integrity": "sha512-CzJYFed23Ow/UK0+cI1FVthDre1p2qc8Q97oizG39d3/SIh3aUHjgj8c60wcR+RSxVO0FzZMXkmq02NmA7vWJg==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@nuxt/webpack/-/webpack-2.17.3.tgz", + "integrity": "sha512-09vP3oShjp4ogsJL3XTi2kk1gh5itG5OwerLxF1NiJNNeuIAc/kei0L3MVhyfMxUVx22SF9sb23cZLIJxoK8cQ==", "dependencies": { - "@babel/core": "^7.14.0", - "@nuxt/babel-preset-app": "2.15.8", - "@nuxt/friendly-errors-webpack-plugin": "^2.5.1", - "@nuxt/utils": "2.15.8", - "babel-loader": "^8.2.2", + "@babel/core": "^7.23.7", + "@nuxt/babel-preset-app": "2.17.3", + "@nuxt/friendly-errors-webpack-plugin": "^2.6.0", + "@nuxt/utils": "2.17.3", + "babel-loader": "^8.3.0", "cache-loader": "^4.1.0", - "caniuse-lite": "^1.0.30001228", - "consola": "^2.15.3", - "css-loader": "^4.3.0", - "cssnano": "^4.1.11", + "caniuse-lite": "^1.0.30001576", + "consola": "^3.2.3", + "css-loader": "^5.2.7", + "cssnano": "^6.0.3", "eventsource-polyfill": "^0.9.6", - "extract-css-chunks-webpack-plugin": "^4.9.0", + "extract-css-chunks-webpack-plugin": "^4.10.0", "file-loader": "^6.2.0", - "glob": "^7.1.7", + "glob": "^8.1.0", "hard-source-webpack-plugin": "^0.13.1", "hash-sum": "^2.0.0", "html-webpack-plugin": "^4.5.1", "lodash": "^4.17.21", "memory-fs": "^0.5.0", - "optimize-css-assets-webpack-plugin": "^5.0.4", + "optimize-css-assets-webpack-plugin": "^6.0.1", "pify": "^5.0.0", - "pnp-webpack-plugin": "^1.6.4", - "postcss": "^7.0.32", - "postcss-import": "^12.0.1", + "pnp-webpack-plugin": "^1.7.0", + "postcss": "^8.4.33", + "postcss-import": "^15.1.0", "postcss-import-resolver": "^2.0.0", - "postcss-loader": "^3.0.0", - "postcss-preset-env": "^6.7.0", - "postcss-url": "^8.0.0", - "semver": "^7.3.5", - "std-env": "^2.3.0", - "style-resources-loader": "^1.4.1", + "postcss-loader": "^4.3.0", + "postcss-preset-env": "^9.3.0", + "postcss-url": "^10.1.3", + "semver": "^7.5.4", + "std-env": "^3.7.0", + "style-resources-loader": "^1.5.0", "terser-webpack-plugin": "^4.2.3", "thread-loader": "^3.0.4", "time-fix-plugin": "^2.0.7", - "ufo": "^0.7.4", + "ufo": "^1.3.2", + "upath": "^2.0.1", "url-loader": "^4.1.1", - "vue-loader": "^15.9.7", + "vue-loader": "^15.11.1", "vue-style-loader": "^4.1.3", - "vue-template-compiler": "^2.6.12", - "webpack": "^4.46.0", - "webpack-bundle-analyzer": "^4.4.1", - "webpack-dev-middleware": "^4.2.0", - "webpack-hot-middleware": "^2.25.0", + "vue-template-compiler": "^2.7.16", + "watchpack": "^2.4.0", + "webpack": "^4.47.0", + "webpack-bundle-analyzer": "^4.10.1", + "webpack-dev-middleware": "^5.0.0", + "webpack-hot-middleware": "^2.26.0", "webpack-node-externals": "^3.0.0", - "webpackbar": "^4.0.0" - } - }, - "node_modules/@nuxt/webpack/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@nuxt/webpack/node_modules/css-loader": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-4.3.0.tgz", - "integrity": "sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg==", - "dependencies": { - "camelcase": "^6.0.0", - "cssesc": "^3.0.0", - "icss-utils": "^4.1.1", - "loader-utils": "^2.0.0", - "postcss": "^7.0.32", - "postcss-modules-extract-imports": "^2.0.0", - "postcss-modules-local-by-default": "^3.0.3", - "postcss-modules-scope": "^2.2.0", - "postcss-modules-values": "^3.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^2.7.1", - "semver": "^7.3.2" + "webpackbar": "^6.0.0" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.27.0 || ^5.0.0" + "node": "^14.18.0 || >=16.10.0" } }, - "node_modules/@nuxt/webpack/node_modules/icss-utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", - "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", - "dependencies": { - "postcss": "^7.0.14" - }, + "node_modules/@nuxt/webpack/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", "engines": { - "node": ">= 6" - } - }, - "node_modules/@nuxt/webpack/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/@nuxt/webpack/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/@nuxt/webpack/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": "^14.18.0 || >=16.10.0" } }, "node_modules/@nuxt/webpack/node_modules/postcss-import": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-12.0.1.tgz", - "integrity": "sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dependencies": { - "postcss": "^7.0.1", - "postcss-value-parser": "^3.2.3", + "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@nuxt/webpack/node_modules/postcss-import/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, - "node_modules/@nuxt/webpack/node_modules/postcss-load-config": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.2.tgz", - "integrity": "sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==", - "dependencies": { - "cosmiconfig": "^5.0.0", - "import-cwd": "^2.0.0" + "node": ">=14.0.0" }, - "engines": { - "node": ">= 4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/@nuxt/webpack/node_modules/postcss-loader": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", - "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", - "dependencies": { - "loader-utils": "^1.1.0", - "postcss": "^7.0.0", - "postcss-load-config": "^2.0.0", - "schema-utils": "^1.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@nuxt/webpack/node_modules/postcss-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@nuxt/webpack/node_modules/postcss-loader/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@nuxt/webpack/node_modules/postcss-modules-extract-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", - "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", - "dependencies": { - "postcss": "^7.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@nuxt/webpack/node_modules/postcss-modules-local-by-default": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", - "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", - "dependencies": { - "icss-utils": "^4.1.1", - "postcss": "^7.0.32", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@nuxt/webpack/node_modules/postcss-modules-scope": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", - "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", - "dependencies": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@nuxt/webpack/node_modules/postcss-modules-values": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", - "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", - "dependencies": { - "icss-utils": "^4.0.0", - "postcss": "^7.0.6" - } - }, - "node_modules/@nuxt/webpack/node_modules/postcss-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-8.0.0.tgz", - "integrity": "sha512-E2cbOQ5aii2zNHh8F6fk1cxls7QVFZjLPSrqvmiza8OuXLzIpErij8BDS5Y3STPfJgpIMNCPEr8JlKQWEoozUw==", - "dependencies": { - "mime": "^2.3.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.0", - "postcss": "^7.0.2", - "xxhashjs": "^0.2.1" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@nuxt/webpack/node_modules/schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "peerDependencies": { + "postcss": "^8.0.0" } }, "node_modules/@nuxtjs/axios": { @@ -2683,286 +3897,6 @@ "workbox-cdn": "^5.1.4" } }, - "node_modules/@nuxtjs/tailwindcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@nuxtjs/tailwindcss/-/tailwindcss-4.2.1.tgz", - "integrity": "sha512-Sku7VETunn5YmvzkXFDuRdP1gUGau02eh5HtcAsI9//1hpSuEN49n3XhatBrPOXQ57WhUlnjXf0/LjT9KQH0+A==", - "dev": true, - "dependencies": { - "@nuxt/postcss8": "^1.1.3", - "autoprefixer": "^10.2.5", - "chalk": "^4.1.1", - "clear-module": "^4.1.1", - "consola": "^2.15.3", - "defu": "^5.0.0", - "postcss": "^8.3.5", - "postcss-custom-properties": "^11.0.0", - "postcss-nesting": "^8.0.1", - "tailwind-config-viewer": "^1.6.2", - "tailwindcss": "^2.2.2", - "ufo": "^0.7.5" - } - }, - "node_modules/@nuxtjs/tailwindcss/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/@nuxtjs/tailwindcss/node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/defu": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-5.0.1.tgz", - "integrity": "sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==", - "dev": true - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/html-tags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", - "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@nuxtjs/tailwindcss/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==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@nuxtjs/tailwindcss/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/@nuxtjs/tailwindcss/node_modules/postcss-js": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", - "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", - "dev": true, - "dependencies": { - "camelcase-css": "^2.0.1", - "postcss": "^8.1.6" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/postcss-nested": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", - "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.6" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/tailwindcss": { - "version": "2.2.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.19.tgz", - "integrity": "sha512-6Ui7JSVtXadtTUo2NtkBBacobzWiQYVjYW0ZnKaP9S1ZCKQ0w7KVNz+YSDI/j7O7KCMHbOkz94ZMQhbT9pOqjw==", - "dev": true, - "dependencies": { - "arg": "^5.0.1", - "bytes": "^3.0.0", - "chalk": "^4.1.2", - "chokidar": "^3.5.2", - "color": "^4.0.1", - "cosmiconfig": "^7.0.1", - "detective": "^5.2.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.7", - "fs-extra": "^10.0.0", - "glob-parent": "^6.0.1", - "html-tags": "^3.1.0", - "is-color-stop": "^1.1.0", - "is-glob": "^4.0.1", - "lodash": "^4.17.21", - "lodash.topath": "^4.5.2", - "modern-normalize": "^1.1.0", - "node-emoji": "^1.11.0", - "normalize-path": "^3.0.0", - "object-hash": "^2.2.0", - "postcss-js": "^3.0.3", - "postcss-load-config": "^3.1.0", - "postcss-nested": "5.0.6", - "postcss-selector-parser": "^6.0.6", - "postcss-value-parser": "^4.1.0", - "pretty-hrtime": "^1.0.3", - "purgecss": "^4.0.3", - "quick-lru": "^5.1.1", - "reduce-css-calc": "^2.1.8", - "resolve": "^1.20.0", - "tmp": "^0.2.1" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "autoprefixer": "^10.0.2", - "postcss": "^8.0.9" - } - }, - "node_modules/@nuxtjs/tailwindcss/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, "node_modules/@nuxtjs/youch": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@nuxtjs/youch/-/youch-4.2.3.tgz", @@ -2973,10 +3907,20 @@ "stack-trace": "0.0.10" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@polka/url": { - "version": "1.0.0-next.21", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", - "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==" + "version": "1.0.0-next.24", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", + "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==" }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", @@ -2996,145 +3940,108 @@ "worker-loader": "^2.0.0" } }, - "node_modules/@teckel/vue-pdf/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "node_modules/@teckel/vue-pdf/node_modules/schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/@teckel/vue-pdf/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" }, "engines": { - "node": ">=4.0.0" + "node": ">= 4" } }, - "node_modules/@types/anymatch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz", - "integrity": "sha512-qLChUo6yhpQ9k905NwL74GU7TxH+9UODwwQ6ICNI+O6EDMExqH/Cv9NsbmcZ7yC/rRXJ/AHCzfgjsFRY5fKjYw==", - "deprecated": "This is a stub types definition. anymatch provides its own type definitions, so you do not need this installed.", + "node_modules/@teckel/vue-pdf/node_modules/worker-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz", + "integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==", "dependencies": { - "anymatch": "*" - } - }, - "node_modules/@types/autoprefixer": { - "version": "9.7.2", - "resolved": "https://registry.npmjs.org/@types/autoprefixer/-/autoprefixer-9.7.2.tgz", - "integrity": "sha512-QX7U7YW3zX3ex6MECtWO9folTGsXeP4b8bSjTq3I1ODM+H+sFHwGKuof+T+qBcDClGlCGtDb3SVfiTVfmcxw4g==", - "dependencies": { - "@types/browserslist": "*", - "postcss": "7.x.x" - } - }, - "node_modules/@types/autoprefixer/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/@types/autoprefixer/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "loader-utils": "^1.0.0", + "schema-utils": "^0.4.0" }, "engines": { - "node": ">=6.0.0" + "node": ">= 6.9.0 || >= 8.9.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "webpack": "^3.0.0 || ^4.0.0-alpha.0 || ^4.0.0" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "engines": { + "node": ">=10.13.0" } }, "node_modules/@types/babel__core": { - "version": "7.1.14", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz", - "integrity": "sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", + "@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.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", - "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", "dependencies": { - "@babel/types": "^7.3.0" + "@babel/types": "^7.20.7" } }, "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dependencies": { "@types/connect": "*", "@types/node": "*" } }, - "node_modules/@types/browserslist": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/@types/browserslist/-/browserslist-4.15.0.tgz", - "integrity": "sha512-h9LyKErRGZqMsHh9bd+FE8yCIal4S0DxKTOeui56VgVXqa66TKiuaIUxCAI7c1O0LjaUzOTcsMyOpO9GetozRA==", - "deprecated": "This is a stub types definition. browserslist provides its own type definitions, so you do not need this installed.", - "dependencies": { - "browserslist": "*" - } - }, "node_modules/@types/clean-css": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.6.tgz", - "integrity": "sha512-Ze1tf+LnGPmG6hBFMi0B4TEB0mhF7EiMM5oyjLDNPE9hxrPU0W+5+bHvO+eFPA+bt0iC1zkQMoU/iGdRVjcRbw==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.11.tgz", + "integrity": "sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw==", "dependencies": { "@types/node": "*", "source-map": "^0.6.0" } }, "node_modules/@types/compression": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.0.tgz", - "integrity": "sha512-3LzWUM+3k3XdWOUk/RO+uSjv7YWOatYq2QADJntK1pjkk4DfVP0KrIEPDnXRJxAAGKe0VpIPRmlINLDuCedZWw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", "dependencies": { "@types/express": "*" } }, "node_modules/@types/connect": { - "version": "3.4.34", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", - "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dependencies": { "@types/node": "*" } @@ -3145,51 +4052,55 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "node_modules/@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/etag": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@types/etag/-/etag-1.8.0.tgz", - "integrity": "sha512-EdSN0x+Y0/lBv7YAb8IU4Jgm6DWM+Bqtz7o5qozl96fzaqdqbdfHS5qjdpFeIv7xQ8jSLyjMMNShgYtMajEHyQ==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@types/etag/-/etag-1.8.3.tgz", + "integrity": "sha512-QYHv9Yeh1ZYSMPQOoxY4XC4F1r+xRUiAriB303F4G6uBsT3KKX60DjiogvVv+2VISVDuJhcIzMdbjT+Bm938QQ==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", "dependencies": { "@types/node": "*", "@types/qs": "*", - "@types/range-parser": "*" + "@types/range-parser": "*", + "@types/send": "*" } }, "node_modules/@types/file-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/file-loader/-/file-loader-5.0.0.tgz", - "integrity": "sha512-evodFzM0PLOXmMZy8DhPN+toP6QgJiIteF6e8iD9T0xGBUllQA/DAb1nZwCIoNh7vuLvqCGPUdsLf3GSbcHd4g==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/file-loader/-/file-loader-5.0.4.tgz", + "integrity": "sha512-aB4X92oi5D2nIGI8/kolnJ47btRM2MQjQS4eJgA/VnCD12x0+kP5v7b5beVQWKHLOcquwUXvv6aMt8PmMy9uug==", "dependencies": { "@types/webpack": "^4" } }, "node_modules/@types/html-minifier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier/-/html-minifier-4.0.0.tgz", - "integrity": "sha512-eFnGhrKmjWBlnSGNtunetE3UU2Tc/LUl92htFslSSTmpp9EKHQVcYQadCyYfnzUEFB5G/3wLWo/USQS/mEPKrA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/html-minifier/-/html-minifier-4.0.5.tgz", + "integrity": "sha512-LfE7f7MFd+YUfZnlBz8W43P4NgSObWiqyKapANsWCj63Aqeqli8/9gVsGP4CwC8jPpTTYlTopKCk9rJSuht/ew==", "dependencies": { "@types/clean-css": "*", "@types/relateurl": "*", @@ -3201,23 +4112,28 @@ "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==" }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, "node_modules/@types/http-proxy": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", - "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/less": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/less/-/less-3.0.2.tgz", - "integrity": "sha512-62vfe65cMSzYaWmpmhqCMMNl0khen89w57mByPi1OseGfcV/LV03fO8YVrNj7rFQsRWNJo650WWyh6m7p8vZmA==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/less/-/less-3.0.6.tgz", + "integrity": "sha512-PecSzorDGdabF57OBeQO/xFbAkYWo88g4Xvnsx7LRwqLC17I7OoKtA3bQB9uXkY6UkMWCOsA8HSVpaoitscdXw==" }, "node_modules/@types/localforage": { "version": "0.0.34", @@ -3229,98 +4145,79 @@ } }, "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/node": { - "version": "12.20.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.12.tgz", - "integrity": "sha512-KQZ1al2hKOONAs2MFv+yTQP1LkDWMrRJ9YCVRalXltOfXsBmH5IownLxQaiq0lnAHwAViLnh2aTYqrPcRGEbgg==" - }, - "node_modules/@types/node-sass": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@types/node-sass/-/node-sass-4.11.3.tgz", - "integrity": "sha512-wXPCn3t9uu5rR4zXNSLasZHQMuRzUKBsdi4MsgT8uq4Lp1gQQo+T2G23tGj4SSgDHeNBle6vGseZtM2XV/X9bw==", + "version": "20.11.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", + "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", "dependencies": { - "@types/node": "*" + "undici-types": "~5.26.4" } }, "node_modules/@types/optimize-css-assets-webpack-plugin": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz", - "integrity": "sha512-PJgbI4KplJfyxKWVrBbEL+rePEBqeozJRMT0mBL3ynhvngASBV/XJ+BneLuJN74RjjMzO0gA5ns80mgubQdZAA==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@types/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.8.tgz", + "integrity": "sha512-n134DdmRVXTy0KKbgg3A/G02r2XJKJicYzbJYhdIO8rdYdzoMv6GNHjog2Oq1ttaCOhsYcPIA6Sn7eFxEGCM1A==", "dependencies": { "@types/webpack": "^4" } }, "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, "node_modules/@types/pug": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.4.tgz", - "integrity": "sha512-cevKhB0yUJCFKzCnkB6HbDRZdYwVRRXzhIKRDgfAR1dnzEwZLRGf5lKpLJLZEP/odmaWT+gWNwH02bRhQIBYPg==" - }, - "node_modules/@types/q": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", - "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", + "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==" }, "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==" }, "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/relateurl": { - "version": "0.2.29", - "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.29.tgz", - "integrity": "sha512-QSvevZ+IRww2ldtfv1QskYsqVVVwCKQf1XbwtcyyoRvLIQzfyPhj/C+3+PKzSDRdiyejaiLgnq//XTkleorpLg==" + "version": "0.2.33", + "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz", + "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==" }, - "node_modules/@types/sass": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.43.1.tgz", - "integrity": "sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/sass-loader": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@types/sass-loader/-/sass-loader-8.0.1.tgz", - "integrity": "sha512-kum0/5Im5K2WdDTRsLtrXXvX2VJc3rgq9favK+vIdWLn35miWUIYuPkiQlLCHks9//sZ3GWYs4uYzCdmoKKLcQ==", - "dependencies": { - "@types/node-sass": "*", - "@types/sass": "*", - "@types/webpack": "^4" - } - }, - "node_modules/@types/serve-static": { - "version": "1.13.9", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", - "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, "node_modules/@types/source-list-map": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", + "integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==" }, "node_modules/@types/tapable": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", - "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==" + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz", + "integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==" }, "node_modules/@types/terser-webpack-plugin": { "version": "4.2.1", @@ -3332,56 +4229,47 @@ } }, "node_modules/@types/uglify-js": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz", - "integrity": "sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g==", + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-Hm/T0kV3ywpJyMGNbsItdivRhYNCQQf1IIsYsXnoVPES4t+FMLyDe0/K+Ea7ahWtMtSNb22ZdY7MIyoD9rqARg==", "dependencies": { "source-map": "^0.6.1" } }, "node_modules/@types/webpack": { - "version": "4.41.28", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.28.tgz", - "integrity": "sha512-Nn84RAiJjKRfPFFCVR8LC4ueTtTdfWAMZ03THIzZWRJB+rX24BD3LqPSFnbMscWauEsT4segAsylPDIaZyZyLQ==", + "version": "4.41.38", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.38.tgz", + "integrity": "sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw==", "dependencies": { - "@types/anymatch": "*", "@types/node": "*", "@types/tapable": "^1", "@types/uglify-js": "*", "@types/webpack-sources": "*", + "anymatch": "^3.0.0", "source-map": "^0.6.0" } }, "node_modules/@types/webpack-bundle-analyzer": { - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.3.tgz", - "integrity": "sha512-l/vaDMWGcXiMB3CbczpyICivLTB07/JNtn1xebsRXE9tPaUDEHgX3x7YP6jfznG5TOu7I4w0Qx1tZz61znmPmg==", + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.5.tgz", + "integrity": "sha512-QlyDyX7rsOIJHASzXWlih8DT9fR+XCG9cwIV/4pKrtScdHv4XFshdEf/7iiqLqG0lzWcoBdzG8ylMHQ5XLNixw==", "dependencies": { "@types/webpack": "^4" } }, - "node_modules/@types/webpack-dev-middleware": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@types/webpack-dev-middleware/-/webpack-dev-middleware-4.1.2.tgz", - "integrity": "sha512-SxXzPCqeZ03fJ2dg3iD7cSXvqZymmS5/2GD9fANRcyWN7HYK1H3ty6q7IInXZKvPrdUqij831G3RLIeKK6aGdw==", - "dependencies": { - "@types/connect": "*", - "@types/webpack": "^4" - } - }, "node_modules/@types/webpack-hot-middleware": { - "version": "2.25.4", - "resolved": "https://registry.npmjs.org/@types/webpack-hot-middleware/-/webpack-hot-middleware-2.25.4.tgz", - "integrity": "sha512-6tQb9EBKIANZYUVLQYWiWfDFVe7FhXSj4bB2EF5QB7VtYWL3HDR+y/zqjZPAnCorv0spLqVMRqjRK8AmhfocMw==", + "version": "2.25.5", + "resolved": "https://registry.npmjs.org/@types/webpack-hot-middleware/-/webpack-hot-middleware-2.25.5.tgz", + "integrity": "sha512-/eRWWMgZteNzl17qLCRdRmtKPZuWy984b11Igz9+BAU5a99Hc2AJinnMohMPVahGRSHby4XwsnjlgIt9m0Ce3g==", "dependencies": { "@types/connect": "*", "@types/webpack": "^4" } }, "node_modules/@types/webpack-sources": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", - "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==", "dependencies": { "@types/node": "*", "@types/source-list-map": "*", @@ -3515,13 +4403,16 @@ } }, "node_modules/@vue/compiler-sfc": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz", - "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", "dependencies": { - "@babel/parser": "^7.18.4", + "@babel/parser": "^7.23.5", "postcss": "^8.4.14", "source-map": "^0.6.1" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" } }, "node_modules/@vue/component-compiler-utils": { @@ -3740,9 +4631,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.9.tgz", - "integrity": "sha512-yceMpm/xd4W2a85iqZyO09gTnHvXF6pyiWjD2jcOJs7hRoZtNNOO1eJlhHj1ixA+xip2hOyGn+LgcvLCMo5zXA==", + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", "engines": { "node": ">=10.0.0" } @@ -3770,10 +4661,9 @@ } }, "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "bin": { "acorn": "bin/acorn" }, @@ -3781,22 +4671,10 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "engines": { "node": ">=0.4.0" } @@ -3836,6 +4714,42 @@ "ajv": ">=5.0.0" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -3844,11 +4758,6 @@ "ajv": "^6.9.1" } }, - "node_modules/alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==" - }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", @@ -3915,10 +4824,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -3937,14 +4852,6 @@ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -3969,6 +4876,18 @@ "node": ">=0.10.0" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -3986,13 +4905,13 @@ } }, "node_modules/array.prototype.reduce": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", - "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.6.tgz", + "integrity": "sha512-UW+Mz8LG/sPSU8jRDCjVr6J/ZKAGpHfwrZ6kWTG5qCxIEiXdVshqGnu5vEZA8S1y6X4aCSbQZ0/EEsfvEvBiSg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-array-method-boxes-properly": "^1.0.0", "is-string": "^1.0.7" }, @@ -4003,6 +4922,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -4020,25 +4959,25 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz", + "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==", "dependencies": { - "object-assign": "^4.1.1", - "util": "0.10.3" + "object.assign": "^4.1.4", + "util": "^0.10.4" } }, "node_modules/assert/node_modules/inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" }, "node_modules/assert/node_modules/util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", "dependencies": { - "inherits": "2.0.1" + "inherits": "2.0.3" } }, "node_modules/assign-symbols": { @@ -4049,25 +4988,23 @@ "node": ">=0.10.0" } }, - "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, "node_modules/async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "optional": true }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, "engines": { "node": ">= 4.0.0" } @@ -4084,10 +5021,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.13", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", - "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", - "dev": true, + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", + "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", "funding": [ { "type": "opencollective", @@ -4096,12 +5032,16 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "browserslist": "^4.21.4", - "caniuse-lite": "^1.0.30001426", - "fraction.js": "^4.2.0", + "browserslist": "^4.22.2", + "caniuse-lite": "^1.0.30001578", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -4116,6 +5056,17 @@ "postcss": "^8.1.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axios": { "version": "0.21.4", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", @@ -4125,9 +5076,9 @@ } }, "node_modules/axios-retry": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.3.1.tgz", - "integrity": "sha512-RohAUQTDxBSWLFEnoIG/6bvmy8l3TfpkclgStjl5MDCMBDgapAWCmr1r/9harQfWC8bzLC8job6UcL1A1Yc+/Q==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.9.1.tgz", + "integrity": "sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==", "dependencies": { "@babel/runtime": "^7.15.4", "is-retry-allowed": "^2.2.0" @@ -4151,6 +5102,30 @@ "webpack": ">=2" } }, + "node_modules/babel-loader/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/babel-loader/node_modules/schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -4169,47 +5144,62 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", + "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.5.0", + "semver": "^6.3.1" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", + "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" + "@babel/helper-define-polyfill-provider": "^0.4.4", + "core-js-compat": "^3.33.1" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", + "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" + "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-syntax-dynamic-import": { @@ -4250,41 +5240,6 @@ "node": ">=0.10.0" } }, - "node_modules/base/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4396,12 +5351,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -4464,25 +5418,28 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", + "elliptic": "^6.5.4", "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 4" } }, "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4520,9 +5477,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "funding": [ { "type": "opencollective", @@ -4531,13 +5488,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -4547,9 +5508,9 @@ } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -4564,9 +5525,11 @@ "url": "https://feross.org/support" } ], + "optional": true, + "peer": true, "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/buffer-from": { @@ -4625,6 +5588,34 @@ "node": ">= 10" } }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/cacache/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4636,6 +5627,17 @@ "node": ">=10" } }, + "node_modules/cacache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/cacache/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -4685,19 +5687,6 @@ "node": ">=0.10.0" } }, - "node_modules/cache-content-type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", - "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", - "dev": true, - "dependencies": { - "mime-types": "^2.1.18", - "ylru": "^1.2.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/cache-loader": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-4.1.0.tgz", @@ -4717,30 +5706,6 @@ "webpack": "^4.0.0" } }, - "node_modules/cache-loader/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/cache-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/cache-loader/node_modules/schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -4759,45 +5724,24 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", - "dependencies": { - "callsites": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", - "dependencies": { - "caller-callsite": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/camel-case": { @@ -4838,9 +5782,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001431", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", - "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==", + "version": "1.0.30001579", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", + "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", "funding": [ { "type": "opencollective", @@ -4849,6 +5793,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -4915,9 +5863,15 @@ } }, "node_modules/ci-info": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz", - "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "engines": { "node": ">=8" } @@ -4945,6 +5899,29 @@ "node": ">=0.10.0" } }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/clean-css": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", @@ -4964,22 +5941,6 @@ "node": ">=6" } }, - "node_modules/clear-module": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/clear-module/-/clear-module-4.1.2.tgz", - "integrity": "sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw==", - "dev": true, - "dependencies": { - "parent-module": "^2.0.0", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -5010,20 +5971,6 @@ "node": ">= 10" } }, - "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/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -5038,85 +5985,6 @@ "node": ">=6" } }, - "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/coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dependencies": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/coa/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==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/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==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/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==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/coa/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==" - }, - "node_modules/coa/node_modules/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==", - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -5129,15 +5997,6 @@ "node": ">=0.10.0" } }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5154,32 +6013,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color/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==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/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==" + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" }, "node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, "node_modules/commander": { "version": "2.20.3", @@ -5192,9 +6034,12 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" }, "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/compressible": { "version": "2.0.18", @@ -5297,6 +6142,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog", "dependencies": { "bluebird": "^3.1.1" }, @@ -5309,51 +6155,10 @@ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==" }, - "node_modules/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==", - "dev": true, - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "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/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==", - "dev": true, - "engines": { - "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==" + "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==" }, "node_modules/cookie": { "version": "0.3.1", @@ -5363,19 +6168,6 @@ "node": ">= 0.6" } }, - "node_modules/cookies": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", - "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", - "dev": true, - "dependencies": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -5398,9 +6190,9 @@ } }, "node_modules/core-js": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz", - "integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==", + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz", + "integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -5408,11 +6200,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz", - "integrity": "sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==", + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz", + "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==", "dependencies": { - "browserslist": "^4.21.4" + "browserslist": "^4.22.2" }, "funding": { "type": "opencollective", @@ -5437,25 +6229,51 @@ } }, "node_modules/cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dependencies": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" }, "engines": { - "node": ">=4" + "node": ">=10" + } + }, + "node_modules/cosmiconfig/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==", + "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/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dependencies": { - "buffer": "^5.1.0" + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/crc/-/crc-4.3.2.tgz", + "integrity": "sha512-uGDHf4KLLh2zsHa8D8hIQ1H/HtFQhyHrc0uhHBcoKGol/Xnb+MPYfUMw7cvON6ze/GUESTudKayDcJC5HnJv1A==", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "buffer": ">=6.0.3" + }, + "peerDependenciesMeta": { + "buffer": { + "optional": true + } } }, "node_modules/create-ecdh": { @@ -5503,9 +6321,9 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, "node_modules/cron-parser": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.1.tgz", - "integrity": "sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", "dependencies": { "luxon": "^3.2.1" }, @@ -5548,139 +6366,64 @@ } }, "node_modules/css-blank-pseudo": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", - "integrity": "sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-6.0.1.tgz", + "integrity": "sha512-goSnEITByxTzU4Oh5oJZrEWudxTqk7L6IXj1UW69pO6Hv0UdX+Vsrt02FFu5DweRh2bLu6WpX/+zsQCu5O1gKw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss": "^7.0.5" - }, - "bin": { - "css-blank-pseudo": "cli.js" + "postcss-selector-parser": "^6.0.13" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/css-blank-pseudo/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/css-blank-pseudo/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==", - "engines": { - "node": "*" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/css-declaration-sorter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", - "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", - "dependencies": { - "postcss": "^7.0.1", - "timsort": "^0.3.0" - }, + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.1.1.tgz", + "integrity": "sha512-dZ3bVTEEc1vxr3Bek9vGwfB5Z6ESPULhcRvO472mfjVnj8jRcTnKO8/JTczlvxM10Myb+wBM++1MtdO76eWcaQ==", "engines": { - "node": ">4" - } - }, - "node_modules/css-declaration-sorter/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/css-declaration-sorter/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.0.9" } }, "node_modules/css-has-pseudo": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz", - "integrity": "sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-6.0.1.tgz", + "integrity": "sha512-WwoVKqNxApfEI7dWFyaHoeFCcUPD+lPyjL6lNpRUNX7IyIUuVpawOTwwA5D0ZR6V2xQZonNPVj8kEcxzEaAQfQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^5.0.0-rc.4" - }, - "bin": { - "css-has-pseudo": "cli.js" + "@csstools/selector-specificity": "^3.0.1", + "postcss-selector-parser": "^6.0.13", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/css-has-pseudo/node_modules/cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", - "bin": { - "cssesc": "bin/cssesc" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/css-has-pseudo/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/css-has-pseudo/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "dependencies": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=4" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/css-loader": { @@ -5710,79 +6453,78 @@ "webpack": "^4.27.0 || ^5.0.0" } }, - "node_modules/css-prefers-color-scheme": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz", - "integrity": "sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==", - "dependencies": { - "postcss": "^7.0.5" - }, + "node_modules/css-loader/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "bin": { - "css-prefers-color-scheme": "cli.js" + "json5": "lib/cli.js" }, "engines": { - "node": ">=6.0.0" + "node": ">=6" } }, - "node_modules/css-prefers-color-scheme/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/css-prefers-color-scheme/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "node_modules/css-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=8.9.0" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-9.0.1.tgz", + "integrity": "sha512-iFit06ochwCKPRiWagbTa1OAWCvWWVdEnIFd8BaRrgO8YrrNh4RAWUQTFcYX5tdFZgFl1DJ3iiULchZyEbnF4g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dependencies": { "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", "nth-check": "^2.0.1" }, "funding": { "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" - }, "node_modules/css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", "dependencies": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" }, "engines": { - "node": ">=8.0.0" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/css-unit-converter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", - "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==", - "dev": true - }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -5795,9 +6537,19 @@ } }, "node_modules/cssdb": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz", - "integrity": "sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==" + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.10.0.tgz", + "integrity": "sha512-yGZ5tmA57gWh/uvdQBHs45wwFY0IBh3ypABk5sEubPBPSzXzkNgsWReqx7gdx6uhC+QoFBe+V8JwBB9/hQ6cIA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ] }, "node_modules/cssesc": { "version": "3.0.0", @@ -5811,189 +6563,111 @@ } }, "node_modules/cssnano": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz", - "integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.3.tgz", + "integrity": "sha512-MRq4CIj8pnyZpcI2qs6wswoYoDD1t0aL28n+41c1Ukcpm56m1h6mCexIHBGjfZfnTqtGSSCP4/fB1ovxgjBOiw==", "dependencies": { - "cosmiconfig": "^5.0.0", - "cssnano-preset-default": "^4.0.8", - "is-resolvable": "^1.0.0", - "postcss": "^7.0.0" + "cssnano-preset-default": "^6.0.3", + "lilconfig": "^3.0.0" }, "engines": { - "node": ">=6.9.0" + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, "node_modules/cssnano-preset-default": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz", - "integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.0.3.tgz", + "integrity": "sha512-4y3H370aZCkT9Ev8P4SO4bZbt+AExeKhh8wTbms/X7OLDo5E7AYUUy6YPxa/uF5Grf+AJwNcCnxKhZynJ6luBA==", "dependencies": { - "css-declaration-sorter": "^4.0.1", - "cssnano-util-raw-cache": "^4.0.1", - "postcss": "^7.0.0", - "postcss-calc": "^7.0.1", - "postcss-colormin": "^4.0.3", - "postcss-convert-values": "^4.0.1", - "postcss-discard-comments": "^4.0.2", - "postcss-discard-duplicates": "^4.0.2", - "postcss-discard-empty": "^4.0.1", - "postcss-discard-overridden": "^4.0.1", - "postcss-merge-longhand": "^4.0.11", - "postcss-merge-rules": "^4.0.3", - "postcss-minify-font-values": "^4.0.2", - "postcss-minify-gradients": "^4.0.2", - "postcss-minify-params": "^4.0.2", - "postcss-minify-selectors": "^4.0.2", - "postcss-normalize-charset": "^4.0.1", - "postcss-normalize-display-values": "^4.0.2", - "postcss-normalize-positions": "^4.0.2", - "postcss-normalize-repeat-style": "^4.0.2", - "postcss-normalize-string": "^4.0.2", - "postcss-normalize-timing-functions": "^4.0.2", - "postcss-normalize-unicode": "^4.0.1", - "postcss-normalize-url": "^4.0.1", - "postcss-normalize-whitespace": "^4.0.2", - "postcss-ordered-values": "^4.1.2", - "postcss-reduce-initial": "^4.0.3", - "postcss-reduce-transforms": "^4.0.2", - "postcss-svgo": "^4.0.3", - "postcss-unique-selectors": "^4.0.1" + "css-declaration-sorter": "^7.1.1", + "cssnano-utils": "^4.0.1", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.0.2", + "postcss-convert-values": "^6.0.2", + "postcss-discard-comments": "^6.0.1", + "postcss-discard-duplicates": "^6.0.1", + "postcss-discard-empty": "^6.0.1", + "postcss-discard-overridden": "^6.0.1", + "postcss-merge-longhand": "^6.0.2", + "postcss-merge-rules": "^6.0.3", + "postcss-minify-font-values": "^6.0.1", + "postcss-minify-gradients": "^6.0.1", + "postcss-minify-params": "^6.0.2", + "postcss-minify-selectors": "^6.0.2", + "postcss-normalize-charset": "^6.0.1", + "postcss-normalize-display-values": "^6.0.1", + "postcss-normalize-positions": "^6.0.1", + "postcss-normalize-repeat-style": "^6.0.1", + "postcss-normalize-string": "^6.0.1", + "postcss-normalize-timing-functions": "^6.0.1", + "postcss-normalize-unicode": "^6.0.2", + "postcss-normalize-url": "^6.0.1", + "postcss-normalize-whitespace": "^6.0.1", + "postcss-ordered-values": "^6.0.1", + "postcss-reduce-initial": "^6.0.2", + "postcss-reduce-transforms": "^6.0.1", + "postcss-svgo": "^6.0.2", + "postcss-unique-selectors": "^6.0.2" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-preset-default/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/cssnano-preset-default/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/cssnano-util-get-arguments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", - "integrity": "sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-util-get-match": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", - "integrity": "sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-util-raw-cache": { + "node_modules/cssnano-utils": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", - "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", - "dependencies": { - "postcss": "^7.0.0" - }, + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.1.tgz", + "integrity": "sha512-6qQuYDqsGoiXssZ3zct6dcMxiqfT6epy7x4R0TQJadd4LWO3sPR6JH6ZByOvVLoZ6EdwPGgd7+DR1EmX3tiXQQ==", "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-util-raw-cache/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/cssnano-util-raw-cache/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/cssnano-util-same-parent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", - "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/cssnano/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, "node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", "dependencies": { - "css-tree": "^1.1.2" + "css-tree": "~2.2.0" }, "engines": { - "node": ">=8.0.0" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" } }, "node_modules/csso/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" }, "engines": { - "node": ">=8.0.0" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" } }, "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" }, "node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/cuint": { "version": "0.2.2", @@ -6001,9 +6675,9 @@ "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==" }, "node_modules/cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.2.tgz", + "integrity": "sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==" }, "node_modules/d": { "version": "1.0.1", @@ -6015,9 +6689,12 @@ } }, "node_modules/date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, "engines": { "node": ">=0.11" }, @@ -6031,6 +6708,11 @@ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==" }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -6048,32 +6730,40 @@ } }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "engines": { "node": ">=0.10" } }, - "node_modules/deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", - "dev": true - }, "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "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==", "engines": { "node": ">=0.10.0" } }, - "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -6085,37 +6775,23 @@ } }, "node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", "dependencies": { - "is-descriptor": "^0.1.0" + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/defu": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/defu/-/defu-3.2.2.tgz", "integrity": "sha512-8UWj5lNv7HD+kB0e9w77Z7TdQlbUYDVWqITLHNqFIn6khrNHv5WQo38Dcm1f6HeNyZf0U7UbPf6WeZDSdCzGDQ==", "dev": true }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6125,18 +6801,18 @@ } }, "node_modules/des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "node_modules/destr": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/destr/-/destr-1.2.1.tgz", - "integrity": "sha512-ud8w0qMLlci6iFG7CNgeRr8OcbUWMsbfjtWft1eJ5Luqrz/M8Ebqk/KCzne8rKUlIQWWfLv0wD6QHrqOf4GshA==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.2.tgz", + "integrity": "sha512-65AlobnZMiCET00KaFFjUefxDX0khFA/E4myqZ7a6Sq1yZtR8+FVIvilVX66vF2uobSumxooYZChiRPCKNqhmg==" }, "node_modules/destroy": { "version": "1.2.0", @@ -6155,23 +6831,6 @@ "node": ">=4" } }, - "node_modules/detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dev": true, - "dependencies": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/devalue": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-2.0.1.tgz", @@ -6224,13 +6883,13 @@ } }, "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, "funding": { "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" @@ -6257,11 +6916,11 @@ ] }, "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dependencies": { - "domelementtype": "^2.2.0" + "domelementtype": "^2.3.0" }, "engines": { "node": ">= 4" @@ -6271,13 +6930,13 @@ } }, "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" @@ -6310,27 +6969,19 @@ } }, "node_modules/dot-case/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/dotenv": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", - "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.2.tgz", + "integrity": "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, "node_modules/duplexer": { @@ -6349,15 +7000,21 @@ "stream-shift": "^1.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "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.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" + "version": "1.4.639", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.639.tgz", + "integrity": "sha512-CkKf3ZUVZchr+zDpAlNLEEy2NJJ9T64ULWaDgy3THXXlPVPkLu3VOs9Bac44nebVtdwl2geSj6AxTtGDOxoXhg==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -6408,9 +7065,9 @@ } }, "node_modules/engine.io": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", - "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -6420,29 +7077,29 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/engine.io-client": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.3.tgz", - "integrity": "sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.2.3", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", "xmlhttprequest-ssl": "~2.0.0" } }, "node_modules/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==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", "engines": { "node": ">=10.0.0" } @@ -6469,9 +7126,12 @@ } }, "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } @@ -6520,34 +7180,49 @@ } }, "node_modules/es-abstract": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", - "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", "dependencies": { - "call-bind": "^1.0.2", + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", "get-symbol-description": "^1.0.0", - "has": "^1.0.3", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -6561,6 +7236,19 @@ "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -6643,18 +7331,6 @@ "node": ">=4.0.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==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -6789,6 +7465,48 @@ "ms": "2.0.0" } }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expand-brackets/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -6808,11 +7526,12 @@ "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" }, "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", "dependencies": { - "is-extendable": "^0.1.0" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "engines": { "node": ">=0.10.0" @@ -6860,47 +7579,31 @@ "node": ">=0.10.0" } }, - "node_modules/extglob/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dependencies": { - "kind-of": "^6.0.0" + "is-extendable": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/extglob/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "engines": { "node": ">=0.10.0" } }, "node_modules/extract-css-chunks-webpack-plugin": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/extract-css-chunks-webpack-plugin/-/extract-css-chunks-webpack-plugin-4.9.0.tgz", - "integrity": "sha512-HNuNPCXRMqJDQ1OHAUehoY+0JVCnw9Y/H22FQzYVwo8Ulgew98AGDu0grnY5c7xwiXHjQa6yJ/1dxLCI/xqTyQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/extract-css-chunks-webpack-plugin/-/extract-css-chunks-webpack-plugin-4.10.0.tgz", + "integrity": "sha512-D/wb/Tbexq8XMBl4uhthto25WBaHI9P8vucDdzwPtLTyVi4Rdw/aiRLSL2rHaF6jZfPAjThWXepFU9PXsdtIbA==", "dependencies": { - "loader-utils": "^2.0.0", + "loader-utils": "^2.0.4", "normalize-url": "1.9.1", "schema-utils": "^1.0.0", "webpack-sources": "^1.1.0" @@ -6912,6 +7615,30 @@ "webpack": "^4.4.0 || ^5.0.0" } }, + "node_modules/extract-css-chunks-webpack-plugin/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/extract-css-chunks-webpack-plugin/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/extract-css-chunks-webpack-plugin/node_modules/schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -6931,9 +7658,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -6951,9 +7678,9 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", "dependencies": { "reusify": "^1.0.4" } @@ -6961,7 +7688,8 @@ "node_modules/figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", - "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==" + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "deprecated": "This module is no longer supported." }, "node_modules/figures": { "version": "3.2.0", @@ -6996,6 +7724,30 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/file-loader/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -7079,12 +7831,6 @@ "flat": "cli.js" } }, - "node_modules/flatten": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", - "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==", - "deprecated": "flatten is deprecated in favor of utility frameworks such as lodash." - }, "node_modules/flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", @@ -7095,9 +7841,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -7113,6 +7859,14 @@ } } }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -7121,17 +7875,44 @@ "node": ">=0.10.0" } }, - "node_modules/fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "engines": { "node": "*" }, "funding": { "type": "patreon", - "url": "https://www.patreon.com/infusion" + "url": "https://github.com/sponsors/rawify" } }, "node_modules/fragment-cache": { @@ -7166,6 +7947,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -7193,9 +7975,9 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", + "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==" }, "node_modules/fs-write-stream-atomic": { "version": "1.0.10", @@ -7214,9 +7996,9 @@ "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==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, "optional": true, "os": [ @@ -7227,19 +8009,22 @@ } }, "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/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -7264,23 +8049,15 @@ "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", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7346,27 +8123,26 @@ } }, "node_modules/git-url-parse": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz", - "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.1.tgz", + "integrity": "sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==", "dependencies": { "git-up": "^7.0.0" } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "dependencies": { "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" + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { - "node": "*" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -7383,6 +8159,11 @@ "node": ">= 6" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -7391,6 +8172,20 @@ "node": ">=4" } }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -7410,10 +8205,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/gzip-size": { "version": "6.0.0", @@ -7429,11 +8235,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/hable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hable/-/hable-3.0.0.tgz", - "integrity": "sha512-7+G0/2/COR8pwteYFqHIVYfQpuEiO2HXwJrhCBJVgrNrl9O5eaUoJVDGXUJX+0RpGncNVTuestexjk1afj01wQ==" - }, "node_modules/hard-source-webpack-plugin": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.13.1.tgz", @@ -7592,9 +8393,9 @@ } }, "node_modules/hard-source-webpack-plugin/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -7610,17 +8411,6 @@ "node": ">=4" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -7638,11 +8428,22 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dependencies": { - "get-intrinsic": "^1.1.1" + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7745,9 +8546,9 @@ } }, "node_modules/hash-base/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -7806,6 +8607,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -7814,15 +8626,10 @@ "he": "bin/he" } }, - "node_modules/hex-color-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" - }, "node_modules/hls.js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.2.7.tgz", - "integrity": "sha512-mD4Po7Q5TPNIYX6G8sDD+RS/xfrWjMjrtp+xPw3Thw8Tq557Vn0wdXIX/Zii28F9ncUMMQPZsGkoCWFna9CZCw==" + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.1.tgz", + "integrity": "sha512-SsUSlpyjOGnwBhVrVEG6vRFPU2SAJ0gUqrFdGeo7YPbOC0vuWK0TDMyp7n3QiaBC/Wkic771uqPnnVdT8/x+3Q==" }, "node_modules/hmac-drbg": { "version": "1.0.1", @@ -7834,20 +8641,30 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/hsl-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==" - }, - "node_modules/hsla-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==" + "node_modules/hookable": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-4.4.1.tgz", + "integrity": "sha512-KWjZM8C7IVT2qne5HTXjM6R6VnRfjfRlf/oCnHd+yFxoHO1DzOl6B9LzV/VqGQK/IrFewq+EG+ePVrE9Tpc3fg==" }, "node_modules/html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "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==" }, "node_modules/html-minifier": { "version": "4.0.0", @@ -7916,9 +8733,9 @@ } }, "node_modules/html-minifier-terser/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/html-tags": { "version": "2.0.0", @@ -7950,30 +8767,6 @@ "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/html-webpack-plugin/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/html-webpack-plugin/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -7992,42 +8785,52 @@ "entities": "^2.0.0" } }, - "node_modules/http-assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", - "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", - "dev": true, + "node_modules/htmlparser2/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", "dependencies": { - "deep-equal": "~1.0.1", - "http-errors": "~1.8.0" + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" }, - "engines": { - "node": ">= 0.8" + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/http-assert/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-assert/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" + "domelementtype": "^2.2.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/http-errors": { @@ -8141,9 +8944,9 @@ "integrity": "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==" }, "node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "engines": { "node": ">= 4" } @@ -8153,52 +8956,36 @@ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, - "node_modules/import-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", - "integrity": "sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg==", + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dependencies": { - "import-from": "^2.1.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "node_modules/import-fresh/node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dependencies": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" + "callsites": "^3.0.0" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/import-fresh/node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-from": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", - "integrity": "sha512-0vdnLL2wSGnhlRmzHJAg5JHjt1l2vYhzJ7tNLGbeVg0fse56tpGaH0uzH+r9Slej+BSXXEHvBKDEnVSLLE9/+w==", - "dependencies": { - "resolve-from": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/import-from/node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "engines": { "node": ">=4" } @@ -8219,11 +9006,6 @@ "node": ">=8" } }, - "node_modules/indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==" - }, "node_modules/infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -8272,12 +9054,12 @@ } }, "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -8289,34 +9071,28 @@ "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" }, - "node_modules/is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", "dependencies": { - "kind-of": "^3.0.2" + "hasown": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", "dependencies": { - "is-buffer": "^1.1.5" + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-arrayish": { @@ -8377,50 +9153,26 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-color-stop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", - "integrity": "sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA==", - "dependencies": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - } - }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", "dependencies": { - "kind-of": "^3.0.2" + "hasown": "^2.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/is-date-object": { @@ -8438,53 +9190,24 @@ } }, "node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dependencies": { + "is-plain-object": "^2.0.4" + }, "engines": { "node": ">=0.10.0" } @@ -8505,21 +9228,6 @@ "node": ">=8" } }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8564,14 +9272,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", @@ -8609,11 +9309,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" - }, "node_modules/is-retry-allowed": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", @@ -8683,16 +9378,18 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-url-superb": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz", - "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==", - "dev": true, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dependencies": { + "which-typed-array": "^1.1.11" + }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakref": { @@ -8714,18 +9411,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -8744,6 +9429,24 @@ "node": ">=0.10.0" } }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest-worker": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", @@ -8764,9 +9467,9 @@ "dev": true }, "node_modules/jiti": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.16.0.tgz", - "integrity": "sha512-L3BJStEf5NAqNuzrpfbN71dp43mYIcBUlCRea/vdyv5dW/AYa1d4bpelko4SHdY3I6eN9Wzyasxirj1/vv5kmg==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "bin": { "jiti": "bin/jiti.js" } @@ -8776,18 +9479,6 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, - "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==", - "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", @@ -8807,8 +9498,7 @@ "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 + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -8816,14 +9506,14 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dependencies": { + "minimist": "^1.2.0" + }, "bin": { "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" } }, "node_modules/jsonfile": { @@ -8848,18 +9538,6 @@ "setimmediate": "^1.0.5" } }, - "node_modules/keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", - "dev": true, - "dependencies": { - "tsscmp": "1.0.6" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -8869,152 +9547,13 @@ } }, "node_modules/klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", "engines": { "node": ">= 8" } }, - "node_modules/koa": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.4.tgz", - "integrity": "sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==", - "dev": true, - "dependencies": { - "accepts": "^1.3.5", - "cache-content-type": "^1.0.0", - "content-disposition": "~0.5.2", - "content-type": "^1.0.4", - "cookies": "~0.8.0", - "debug": "^4.3.2", - "delegates": "^1.0.0", - "depd": "^2.0.0", - "destroy": "^1.0.4", - "encodeurl": "^1.0.2", - "escape-html": "^1.0.3", - "fresh": "~0.5.2", - "http-assert": "^1.3.0", - "http-errors": "^1.6.3", - "is-generator-function": "^1.0.7", - "koa-compose": "^4.1.0", - "koa-convert": "^2.0.0", - "on-finished": "^2.3.0", - "only": "~0.0.2", - "parseurl": "^1.3.2", - "statuses": "^1.5.0", - "type-is": "^1.6.16", - "vary": "^1.1.2" - }, - "engines": { - "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" - } - }, - "node_modules/koa-compose": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", - "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", - "dev": true - }, - "node_modules/koa-convert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", - "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", - "dev": true, - "dependencies": { - "co": "^4.6.0", - "koa-compose": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/koa-send": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", - "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "http-errors": "^1.7.3", - "resolve-path": "^1.4.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/koa-send/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/koa-send/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/koa-static": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", - "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", - "dev": true, - "dependencies": { - "debug": "^3.1.0", - "koa-send": "^5.0.0" - }, - "engines": { - "node": ">= 7.6.0" - } - }, - "node_modules/koa-static/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/koa/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/koa/node_modules/http-errors/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/last-call-webpack-plugin": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", @@ -9025,20 +9564,20 @@ } }, "node_modules/launch-editor": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", "dependencies": { "picocolors": "^1.0.0", - "shell-quote": "^1.7.3" + "shell-quote": "^1.8.1" } }, "node_modules/launch-editor-middleware": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor-middleware/-/launch-editor-middleware-2.6.0.tgz", - "integrity": "sha512-K2yxgljj5TdCeRN1lBtO3/J26+AIDDDw+04y6VAiZbWcTdBwsYN6RrZBnW5DN/QiSIdKNjKdATLUUluWWFYTIA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor-middleware/-/launch-editor-middleware-2.6.1.tgz", + "integrity": "sha512-Fg/xYhf7ARmRp40n18wIfJyuAMEjXo67Yull7uF7d0OJ3qA4EYJISt1XfPPn69IIJ5jKgQwzcg6DqHYo95LL/g==", "dependencies": { - "launch-editor": "^2.6.0" + "launch-editor": "^2.6.1" } }, "node_modules/libarchive.js": { @@ -9055,19 +9594,17 @@ } }, "node_modules/lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", "engines": { - "node": ">=10" + "node": ">=14" } }, "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 + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/loader-runner": { "version": "4.3.0", @@ -9078,16 +9615,16 @@ } }, "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", - "json5": "^2.1.2" + "json5": "^1.0.1" }, "engines": { - "node": ">=8.9.0" + "node": ">=4.0.0" } }, "node_modules/localforage": { @@ -9159,12 +9696,6 @@ "lodash._reinterpolate": "^3.0.0" } }, - "node_modules/lodash.topath": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", - "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", - "dev": true - }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -9184,9 +9715,9 @@ } }, "node_modules/luxon": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", - "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { "node": ">=12" } @@ -9206,24 +9737,13 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, - "node_modules/map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dependencies": { - "p-defer": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -9243,6 +9763,18 @@ "node": ">=0.10.0" } }, + "node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/marks-pane": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/marks-pane/-/marks-pane-1.0.9.tgz", @@ -9259,48 +9791,16 @@ } }, "node_modules/mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" - }, - "node_modules/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==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mem": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", - "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", - "dependencies": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/mem?sponsor=1" - } - }, - "node_modules/mem/node_modules/mimic-fn": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", - "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", - "engines": { - "node": ">=8" - } + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, "node_modules/memfs": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.11.tgz", - "integrity": "sha512-GvsCITGAyDCxxsJ+X6prJexFQEhOCJaIlUbsAvjzSI5o5O7j2dle3jWvz5Z5aOdpOxW6ol3vI1+0ut+641F1+w==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", "dependencies": { - "fs-monkey": "^1.0.3" + "fs-monkey": "^1.0.4" }, "engines": { "node": ">= 4.0.0" @@ -9339,15 +9839,6 @@ "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -9426,28 +9917,28 @@ "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=10" } }, "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dependencies": { "yallist": "^4.0.0" }, @@ -9542,17 +10033,6 @@ "node": ">=0.10.0" } }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -9564,18 +10044,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/modern-normalize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", - "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -9590,9 +10058,9 @@ } }, "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "engines": { "node": ">=10" } @@ -9618,16 +10086,33 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", "optional": true }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -9656,76 +10141,6 @@ "node": ">=0.10.0" } }, - "node_modules/nanomatch/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -9752,19 +10167,10 @@ "lower-case": "^1.1.1" } }, - "node_modules/node-emoji": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", - "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", - "dev": true, - "dependencies": { - "lodash": "^4.17.21" - } - }, "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -9780,12 +10186,17 @@ } } }, + "node_modules/node-fetch-native": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.1.tgz", + "integrity": "sha512-bW9T/uJDPAJB2YNYEpWzE54U5O3MQidXsOyTfnbKYtTtFexRvGzb1waphBN4ZwP6EcIvYYEOwW0b72BpAqydTw==" + }, "node_modules/node-html-parser": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-3.3.6.tgz", - "integrity": "sha512-VkWDHvNgFGB3mbQGMyzqRE1i/BG7TKX9wRXC8e/v8kL0kZR/Oy6RjYxXH91K6/+m3g8iQ8dTqRy75lTYoA2Cjg==", + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.12.tgz", + "integrity": "sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==", "dependencies": { - "css-select": "^4.1.3", + "css-select": "^5.1.0", "he": "1.2.0" } }, @@ -9843,9 +10254,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/node-res": { "version": "5.0.1", @@ -9911,32 +10322,27 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==" - }, "node_modules/nuxt": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-2.15.8.tgz", - "integrity": "sha512-ceK3qLg/Baj7J8mK9bIxqw9AavrF+LXqwYEreBdY/a4Sj8YV4mIvhqea/6E7VTCNNGvKT2sJ/TTJjtfQ597lTA==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-2.17.3.tgz", + "integrity": "sha512-mQUy0J2DYYxHZvgBX8YvrrM8sKUhBqBxcQ0ePjy7cdyTaDAN8QeOLrizINm7NVPMrFGLYurhp5rbX3/qyQcKyg==", "hasInstallScript": true, "dependencies": { - "@nuxt/babel-preset-app": "2.15.8", - "@nuxt/builder": "2.15.8", - "@nuxt/cli": "2.15.8", - "@nuxt/components": "^2.1.8", - "@nuxt/config": "2.15.8", - "@nuxt/core": "2.15.8", - "@nuxt/generator": "2.15.8", - "@nuxt/loading-screen": "^2.0.3", - "@nuxt/opencollective": "^0.3.2", - "@nuxt/server": "2.15.8", - "@nuxt/telemetry": "^1.3.3", - "@nuxt/utils": "2.15.8", - "@nuxt/vue-app": "2.15.8", - "@nuxt/vue-renderer": "2.15.8", - "@nuxt/webpack": "2.15.8" + "@nuxt/babel-preset-app": "2.17.3", + "@nuxt/builder": "2.17.3", + "@nuxt/cli": "2.17.3", + "@nuxt/components": "^2.2.1", + "@nuxt/config": "2.17.3", + "@nuxt/core": "2.17.3", + "@nuxt/generator": "2.17.3", + "@nuxt/loading-screen": "^2.0.4", + "@nuxt/opencollective": "^0.4.0", + "@nuxt/server": "2.17.3", + "@nuxt/telemetry": "^1.5.0", + "@nuxt/utils": "2.17.3", + "@nuxt/vue-app": "2.17.3", + "@nuxt/vue-renderer": "2.17.3", + "@nuxt/webpack": "2.17.3" }, "bin": { "nuxt": "bin/nuxt.js" @@ -9954,6 +10360,45 @@ "tiny-emitter": "^2.1.0" } }, + "node_modules/nuxt-socket-io/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nuxt-socket-io/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nuxt-socket-io/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9975,6 +10420,29 @@ "node": ">=0.10.0" } }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object-copy/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -9996,9 +10464,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10023,12 +10491,12 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -10040,14 +10508,15 @@ } }, "node_modules/object.getownpropertydescriptors": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", - "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.7.tgz", + "integrity": "sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g==", "dependencies": { - "array.prototype.reduce": "^1.0.5", + "array.prototype.reduce": "^1.0.6", "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "safe-array-concat": "^1.0.0" }, "engines": { "node": ">= 0.8" @@ -10067,22 +10536,6 @@ "node": ">=0.10.0" } }, - "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -10124,28 +10577,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/only": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", - "dev": true - }, - "node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -10155,17 +10586,633 @@ } }, "node_modules/optimize-css-assets-webpack-plugin": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.8.tgz", - "integrity": "sha512-mgFS1JdOtEGzD8l+EuISqL57cKO+We9GcoiQEmdCWRqqck+FGNmYJtx9qfAPzEz+lRrlThWMuGDaRkI/yWNx/Q==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-6.0.1.tgz", + "integrity": "sha512-BshV2UZPfggZLdUfN3zFBbG4sl/DynUI+YCB6fRRDWaqO2OiWN8GPcp4Y0/fEV6B3k9Hzyk3czve3V/8B/SzKQ==", "dependencies": { - "cssnano": "^4.1.10", - "last-call-webpack-plugin": "^3.0.0" + "cssnano": "^5.0.2", + "last-call-webpack-plugin": "^3.0.0", + "postcss": "^8.2.1" }, "peerDependencies": { "webpack": "^4.0.0" } }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/css-declaration-sorter": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", + "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/cssnano": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", + "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "dependencies": { + "cssnano-preset-default": "^5.2.14", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/cssnano-preset-default": { + "version": "5.2.14", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", + "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.1", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.4", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.2", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/optimize-css-assets-webpack-plugin/node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -10179,14 +11226,6 @@ "node": ">=0.10.0" } }, - "node_modules/p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", - "engines": { - "node": ">=4" - } - }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -10257,27 +11296,6 @@ "no-case": "^2.2.0" } }, - "node_modules/parent-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-2.0.0.tgz", - "integrity": "sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg==", - "dev": true, - "dependencies": { - "callsites": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/parent-module/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/parse-asn1": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", @@ -10365,9 +11383,9 @@ } }, "node_modules/pascal-case/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/pascalcase": { "version": "0.1.1", @@ -10417,11 +11435,39 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/path-to-regexp": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", - "dev": true + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } }, "node_modules/path-type": { "version": "4.0.0", @@ -10452,9 +11498,12 @@ } }, "node_modules/pdfjs-dist": { - "version": "2.6.347", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.6.347.tgz", - "integrity": "sha512-QC+h7hG2su9v/nU1wEI3SnpPIrqJODL7GTDFvR74ANKGq1AFJW16PH8VWnhpiTi9YcLSFV9xLeWSgq+ckHLdVQ==" + "version": "2.7.570", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.7.570.tgz", + "integrity": "sha512-/ZkA1FwkEOyDaq11JhMLazdwQAA0F9uwrP7h/1L9Akt9KWh1G5/tkzS+bPuUELq2s2GDFnaT+kooN/aSjT7DXQ==", + "peerDependencies": { + "worker-loader": "^3.0.7" + } }, "node_modules/picocolors": { "version": "1.0.0", @@ -10483,6 +11532,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -10505,29 +11563,6 @@ "node": ">=6" } }, - "node_modules/portfinder": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", - "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", - "dev": true, - "dependencies": { - "async": "^2.6.4", - "debug": "^3.2.7", - "mkdirp": "^0.5.6" - }, - "engines": { - "node": ">= 0.12.0" - } - }, - "node_modules/portfinder/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -10537,9 +11572,9 @@ } }, "node_modules/postcss": { - "version": "8.4.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", - "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==", + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "funding": [ { "type": "opencollective", @@ -10548,10 +11583,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -10560,944 +11599,432 @@ } }, "node_modules/postcss-attribute-case-insensitive": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz", - "integrity": "sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-6.0.2.tgz", + "integrity": "sha512-IRuCwwAAQbgaLhxQdQcIIK0dCVXg3XDUnzgKD8iwdiYdwU4rMWRWyl/W9/0nA4ihVpq5pyALiHB2veBJ0292pw==", "dependencies": { - "postcss": "^7.0.2", - "postcss-selector-parser": "^6.0.2" - } - }, - "node_modules/postcss-attribute-case-insensitive/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-attribute-case-insensitive/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "postcss-selector-parser": "^6.0.10" }, "engines": { - "node": ">=6.0.0" + "node": "^14 || ^16 || >=18" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-calc": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", - "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", "dependencies": { - "postcss": "^7.0.27", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.2" - } - }, - "node_modules/postcss-calc/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-calc/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.0.0" + "node": "^14 || ^16 || >=18.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" } }, "node_modules/postcss-color-functional-notation": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz", - "integrity": "sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-6.0.4.tgz", + "integrity": "sha512-YBzfVvVUNR4U3N0imzU1NPKCuwxzfHJkEP6imJxzsJ8LozRKeej9mWmg9Ef1ovJdb0xrGTRVzUxgTrMun5iw/Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" + "@csstools/css-color-parser": "^1.5.1", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "@csstools/postcss-progressive-custom-properties": "^3.0.3" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-color-functional-notation/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-color-functional-notation/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-color-functional-notation/node_modules/postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "dependencies": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=6.14.4" - } - }, - "node_modules/postcss-color-gray": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz", - "integrity": "sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==", - "dependencies": { - "@csstools/convert-colors": "^1.4.0", - "postcss": "^7.0.5", - "postcss-values-parser": "^2.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-color-gray/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-color-gray/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-color-gray/node_modules/postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "dependencies": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=6.14.4" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-color-hex-alpha": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz", - "integrity": "sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-9.0.3.tgz", + "integrity": "sha512-7sEHU4tAS6htlxun8AB9LDrCXoljxaC34tFVRlYKcvO+18r5fvGiXgv5bQzN40+4gXLCyWSMRK5FK31244WcCA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss": "^7.0.14", - "postcss-values-parser": "^2.0.1" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-color-hex-alpha/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-color-hex-alpha/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-color-hex-alpha/node_modules/postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "dependencies": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=6.14.4" - } - }, - "node_modules/postcss-color-mod-function": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz", - "integrity": "sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==", - "dependencies": { - "@csstools/convert-colors": "^1.4.0", - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-color-mod-function/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-color-mod-function/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-color-mod-function/node_modules/postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "dependencies": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=6.14.4" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-color-rebeccapurple": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz", - "integrity": "sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-9.0.2.tgz", + "integrity": "sha512-f+RDEAPW2m8UbJWkSpRfV+QxhSaQhDMihI75DVGJJh4oRIoegjheeRtINFJum9D8BqGJcvD4GLjggTvCwZ4zuA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-color-rebeccapurple/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-color-rebeccapurple/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-color-rebeccapurple/node_modules/postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "dependencies": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=6.14.4" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-colormin": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", - "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.0.2.tgz", + "integrity": "sha512-TXKOxs9LWcdYo5cgmcSHPkyrLAh86hX1ijmyy6J8SbOhyv6ua053M3ZAM/0j44UsnQNIWdl8gb5L7xX2htKeLw==", "dependencies": { - "browserslist": "^4.0.0", - "color": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "browserslist": "^4.22.2", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-colormin/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-colormin/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/postcss-colormin/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/postcss-convert-values": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", - "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.0.2.tgz", + "integrity": "sha512-aeBmaTnGQ+NUSVQT8aY0sKyAD/BaLJenEKZ03YK0JnDE1w1Rr8XShoxdal2V2H26xTJKr3v5haByOhJuyT4UYw==", "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "browserslist": "^4.22.2", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-convert-values/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-convert-values/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/postcss-convert-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/postcss-custom-media": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz", - "integrity": "sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.2.tgz", + "integrity": "sha512-zcEFNRmDm2fZvTPdI1pIW3W//UruMcLosmMiCdpQnrCsTRzWlKQPYMa1ud9auL0BmrryKK1+JjIGn19K0UjO/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss": "^7.0.14" + "@csstools/cascade-layer-name-parser": "^1.0.5", + "@csstools/css-parser-algorithms": "^2.3.2", + "@csstools/css-tokenizer": "^2.2.1", + "@csstools/media-query-list-parser": "^2.1.5" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-custom-media/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-custom-media/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-custom-properties": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-11.0.0.tgz", - "integrity": "sha512-Fhnx/QLt+CTt23A/KKVx1anZD9nmVpOxKCKv5owWacMoOsBXFhMAD6SZYbmPMH4nHdIeMUnWOvLZnlY4niS0sA==", - "dev": true, + "version": "13.3.4", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-13.3.4.tgz", + "integrity": "sha512-9YN0gg9sG3OH+Z9xBrp2PWRb+O4msw+5Sbp3ZgqrblrwKspXVQe5zr5sVqi43gJGwW/Rv1A483PRQUzQOEewvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss-values-parser": "^4.0.0" + "@csstools/cascade-layer-name-parser": "^1.0.7", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=10.0.0" + "node": "^14 || ^16 || >=18" }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-7.1.6.tgz", + "integrity": "sha512-svsjWRaxqL3vAzv71dV0/65P24/FB8TbPX+lWyyf9SZ7aZm4S4NhCn7N3Bg+Z5sZunG3FS8xQ80LrCU9hb37cw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/cascade-layer-name-parser": "^1.0.5", + "@csstools/css-parser-algorithms": "^2.3.2", + "@csstools/css-tokenizer": "^2.2.1", + "postcss-selector-parser": "^6.0.13" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-8.0.1.tgz", + "integrity": "sha512-uULohfWBBVoFiZXgsQA24JV6FdKIidQ+ZqxOouhWwdE+qJlALbkS5ScB43ZTjPK+xUZZhlaO/NjfCt5h4IKUfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.0.13" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.1.tgz", + "integrity": "sha512-f1KYNPtqYLUeZGCHQPKzzFtsHaRuECe6jLakf/RjSRqvF5XHLZnM2+fXLhb8Qh/HBFHs3M4cSLb1k3B899RYIg==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.1.tgz", + "integrity": "sha512-1hvUs76HLYR8zkScbwyJ8oJEugfPV+WchpnA+26fpJ7Smzs51CzGBHC32RS03psuX/2l0l0UKh2StzNxOrKCYg==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.1.tgz", + "integrity": "sha512-yitcmKwmVWtNsrrRqGJ7/C0YRy53i0mjexBDQ9zYxDwTWVBgbU4+C9jIZLmQlTDT9zhml+u0OMFJh8+31krmOg==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.1.tgz", + "integrity": "sha512-qs0ehZMMZpSESbRkw1+inkf51kak6OOzNRaoLd/U7Fatp0aN2HQ1rxGOrJvYcRAN9VpX8kUF13R2ofn8OlvFVA==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-5.0.3.tgz", + "integrity": "sha512-QKYpwmaSm6HcdS0ndAuWSNNMv78R1oSySoh3mYBmctHWr2KWcwPJVakdOyU4lvFVW0GRu9wfIQwGeM4p3xU9ow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^3.0.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-9.0.1.tgz", + "integrity": "sha512-N2VQ5uPz3Z9ZcqI5tmeholn4d+1H14fKXszpjogZIrFbhaq0zNAtq8sAnw6VLiqGbL8YBzsnu7K9bBkTqaRimQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.0.13" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-8.0.1.tgz", + "integrity": "sha512-NFU3xcY/xwNaapVb+1uJ4n23XImoC86JNwkY/uduytSl2s9Ekc2EpzmRR63+ExitnW3Mab3Fba/wRPCT5oDILA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.0.13" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", "peerDependencies": { "postcss": "^8.1.0" } }, - "node_modules/postcss-custom-selectors": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz", - "integrity": "sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==", - "dependencies": { - "postcss": "^7.0.2", - "postcss-selector-parser": "^5.0.0-rc.3" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-custom-selectors/node_modules/cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-custom-selectors/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-custom-selectors/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "dependencies": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-dir-pseudo-class": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz", - "integrity": "sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==", - "dependencies": { - "postcss": "^7.0.2", - "postcss-selector-parser": "^5.0.0-rc.3" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/postcss-dir-pseudo-class/node_modules/cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-dir-pseudo-class/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-dir-pseudo-class/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "dependencies": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-discard-comments": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", - "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-comments/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-discard-comments/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", - "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-duplicates/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-discard-duplicates/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-discard-empty": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", - "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-empty/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-discard-empty/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", - "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-overridden/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-discard-overridden/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-double-position-gradients": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz", - "integrity": "sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==", - "dependencies": { - "postcss": "^7.0.5", - "postcss-values-parser": "^2.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-double-position-gradients/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-double-position-gradients/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-double-position-gradients/node_modules/postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "dependencies": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=6.14.4" - } - }, - "node_modules/postcss-env-function": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-2.0.2.tgz", - "integrity": "sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==", - "dependencies": { - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-env-function/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-env-function/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-env-function/node_modules/postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "dependencies": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=6.14.4" - } - }, - "node_modules/postcss-focus-visible": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz", - "integrity": "sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==", - "dependencies": { - "postcss": "^7.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-focus-visible/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-focus-visible/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-focus-within": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz", - "integrity": "sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==", - "dependencies": { - "postcss": "^7.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-focus-within/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-focus-within/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-font-variant": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz", - "integrity": "sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA==", - "dependencies": { - "postcss": "^7.0.2" - } - }, - "node_modules/postcss-font-variant/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-font-variant/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, "node_modules/postcss-gap-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz", - "integrity": "sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==", - "dependencies": { - "postcss": "^7.0.2" - }, + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-5.0.1.tgz", + "integrity": "sha512-k2z9Cnngc24c0KF4MtMuDdToROYqGMMUQGcE6V0odwjHyOHtaDBlLeRBV70y9/vF7KIbShrTRZ70JjsI1BZyWw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-gap-properties/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-gap-properties/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-image-set-function": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz", - "integrity": "sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-6.0.2.tgz", + "integrity": "sha512-/O1xwqpJiz/apxGQi7UUfv1xUcorvkHZfvCYHPpRxxZj2WvjD0rg0+/+c+u5/Do5CpUg3XvfYxMrhcnjW1ArDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-image-set-function/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-image-set-function/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-image-set-function/node_modules/postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "dependencies": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=6.14.4" - } - }, - "node_modules/postcss-import": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-13.0.0.tgz", - "integrity": "sha512-LPUbm3ytpYopwQQjqgUH4S3EM/Gb9QsaSPP/5vnoi+oKVy3/mIk2sc0Paqw7RL57GpScm9MdIMUypw2znWiBpg==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=10.0.0" + "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "postcss": "^8.0.0" + "postcss": "^8.4" } }, "node_modules/postcss-import-resolver": { @@ -11508,39 +12035,10 @@ "enhanced-resolve": "^4.1.1" } }, - "node_modules/postcss-initial": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.4.tgz", - "integrity": "sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg==", - "dependencies": { - "postcss": "^7.0.2" - } - }, - "node_modules/postcss-initial/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-initial/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, "node_modules/postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, "dependencies": { "camelcase-css": "^2.0.1" @@ -11553,71 +12051,57 @@ "url": "https://opencollective.com/postcss/" }, "peerDependencies": { - "postcss": "^8.3.3" + "postcss": "^8.4.21" } }, "node_modules/postcss-lab-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz", - "integrity": "sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==", + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-6.0.9.tgz", + "integrity": "sha512-PKFAVTBEWJYsoSTD7Kp/OzeiMsXaLX39Pv75XgUyF5VrbMfeTw+JqCGsvDP3dPhclh6BemdCFHcjXBG9gO4UCg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "@csstools/convert-colors": "^1.4.0", - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" + "@csstools/css-color-parser": "^1.5.1", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "@csstools/postcss-progressive-custom-properties": "^3.0.3" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-lab-function/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-lab-function/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-lab-function/node_modules/postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "dependencies": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=6.14.4" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" }, "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": ">= 14" }, "peerDependencies": { "postcss": ">=8.0.9", @@ -11632,11 +12116,19 @@ } } }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/postcss-loader": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-4.3.0.tgz", "integrity": "sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q==", - "dev": true, "dependencies": { "cosmiconfig": "^7.0.0", "klona": "^2.0.4", @@ -11656,406 +12148,144 @@ "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/postcss-loader/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/postcss-loader/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/postcss-loader/node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/postcss-loader/node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" + "node_modules/postcss-loader/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" }, "engines": { "node": ">=6" } }, - "node_modules/postcss-loader/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, + "node_modules/postcss-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "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" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/postcss-loader/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" + "node": ">=8.9.0" } }, "node_modules/postcss-logical": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-3.0.0.tgz", - "integrity": "sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-7.0.1.tgz", + "integrity": "sha512-8GwUQZE0ri0K0HJHkDv87XOLC8DE0msc+HoWLeKdtjDZEwpZ5xuK3QdV6FhmHSQW40LPkg43QzvATRAI3LsRkg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss": "^7.0.2" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-logical/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-logical/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-media-minmax": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz", - "integrity": "sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==", - "dependencies": { - "postcss": "^7.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-media-minmax/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-media-minmax/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-merge-longhand": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", - "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.2.tgz", + "integrity": "sha512-+yfVB7gEM8SrCo9w2lCApKIEzrTKl5yS1F4yGhV3kSim6JzbfLGJyhR1B6X+6vOT0U33Mgx7iv4X9MVWuaSAfw==", "dependencies": { - "css-color-names": "0.0.4", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "stylehacks": "^4.0.0" + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.0.2" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-merge-longhand/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-merge-longhand/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/postcss-merge-longhand/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/postcss-merge-rules": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", - "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.0.3.tgz", + "integrity": "sha512-yfkDqSHGohy8sGYIJwBmIGDv4K4/WrJPX355XrxQb/CSsT4Kc/RxDi6akqn5s9bap85AWgv21ArcUWwWdGNSHA==", "dependencies": { - "browserslist": "^4.0.0", + "browserslist": "^4.22.2", "caniuse-api": "^3.0.0", - "cssnano-util-same-parent": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0", - "vendors": "^1.0.0" + "cssnano-utils": "^4.0.1", + "postcss-selector-parser": "^6.0.15" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-merge-rules/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-merge-rules/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dependencies": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=8" + "peerDependencies": { + "postcss": "^8.4.31" } }, "node_modules/postcss-minify-font-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", - "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.0.1.tgz", + "integrity": "sha512-tIwmF1zUPoN6xOtA/2FgVk1ZKrLcCvE0dpZLtzyyte0j9zUeB8RTbCqrHZGjJlxOvNWKMYtunLrrl7HPOiR46w==", "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-font-values/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-minify-font-values/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/postcss-minify-font-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/postcss-minify-gradients": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", - "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.1.tgz", + "integrity": "sha512-M1RJWVjd6IOLPl1hYiOd5HQHgpp6cvJVLrieQYS9y07Yo8itAr6jaekzJphaJFR0tcg4kRewCk3kna9uHBxn/w==", "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "is-color-stop": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "colord": "^2.9.1", + "cssnano-utils": "^4.0.1", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-gradients/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-minify-gradients/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/postcss-minify-gradients/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/postcss-minify-params": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", - "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.0.2.tgz", + "integrity": "sha512-zwQtbrPEBDj+ApELZ6QylLf2/c5zmASoOuA4DzolyVGdV38iR2I5QRMsZcHkcdkZzxpN8RS4cN7LPskOkTwTZw==", "dependencies": { - "alphanum-sort": "^1.0.0", - "browserslist": "^4.0.0", - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "uniqs": "^2.0.0" + "browserslist": "^4.22.2", + "cssnano-utils": "^4.0.1", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-params/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-minify-params/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/postcss-minify-params/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/postcss-minify-selectors": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", - "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.2.tgz", + "integrity": "sha512-0b+m+w7OAvZejPQdN2GjsXLv5o0jqYHX3aoV0e7RBKPCsB7TYG5KKWBFhGnB/iP3213Ts8c5H4wLPLMm7z28Sg==", "dependencies": { - "alphanum-sort": "^1.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" + "postcss-selector-parser": "^6.0.15" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-selectors/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-minify-selectors/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dependencies": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=8" + "peerDependencies": { + "postcss": "^8.4.31" } }, "node_modules/postcss-modules-extract-imports": { @@ -12070,9 +12300,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^6.0.2", @@ -12086,9 +12316,9 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", "dependencies": { "postcss-selector-parser": "^6.0.4" }, @@ -12114,12 +12344,12 @@ } }, "node_modules/postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^6.0.11" }, "engines": { "node": ">=12.0" @@ -12133,867 +12363,412 @@ } }, "node_modules/postcss-nesting": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-8.0.1.tgz", - "integrity": "sha512-cHPNhW5VvRQjszFDxmy16mis9qFQqQLBNw6KVmueLqqE3M182ZAk9+QoxGqbGVryzLVhannw2B5Yhosqq522fA==", - "dev": true, - "engines": { - "node": "12 - 16" + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.0.2.tgz", + "integrity": "sha512-63PpJHSeNs93S3ZUIyi+7kKx4JqOIEJ6QYtG3x+0qA4J03+4n0iwsyA1GAHyWxsHYljQS4/4ZK1o2sMi70b5wQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/selector-specificity": "^3.0.1", + "postcss-selector-parser": "^6.0.13" }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.1.tgz", + "integrity": "sha512-aW5LbMNRZ+oDV57PF9K+WI1Z8MPnF+A8qbajg/T8PP126YrGX1f9IQx21GI2OlGz7XFJi/fNi0GTbY948XJtXg==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.1.tgz", + "integrity": "sha512-mc3vxp2bEuCb4LgCcmG1y6lKJu1Co8T+rKHrcbShJwUmKJiEl761qb/QQCfFwlrvSeET3jksolCR/RZuMURudw==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.1.tgz", + "integrity": "sha512-HRsq8u/0unKNvm0cvwxcOUEcakFXqZ41fv3FOdPn916XFUrympjr+03oaLkuZENz3HE9RrQE9yU0Xv43ThWjQg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.1.tgz", + "integrity": "sha512-Gbb2nmCy6tTiA7Sh2MBs3fj9W8swonk6lw+dFFeQT68B0Pzwp1kvisJQkdV6rbbMSd9brMlS8I8ts52tAGWmGQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.1.tgz", + "integrity": "sha512-5Fhx/+xzALJD9EI26Aq23hXwmv97Zfy2VFrt5PLT8lAhnBIZvmaT5pQk+NuJ/GWj/QWaKSKbnoKDGLbV6qnhXg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.1.tgz", + "integrity": "sha512-4zcczzHqmCU7L5dqTB9rzeqPWRMc0K2HoR+Bfl+FSMbqGBUcP5LRfgcH4BdRtLuzVQK1/FHdFoGT3F7rkEnY+g==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.0.2.tgz", + "integrity": "sha512-Ff2VdAYCTGyMUwpevTZPZ4w0+mPjbZzLLyoLh/RMpqUqeQKZ+xMm31hkxBavDcGKcxm6ACzGk0nBfZ8LZkStKA==", + "dependencies": { + "browserslist": "^4.22.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.1.tgz", + "integrity": "sha512-jEXL15tXSvbjm0yzUV7FBiEXwhIa9H88JOXDGQzmcWoB4mSjZIsmtto066s2iW9FYuIrIF4k04HA2BKAOpbsaQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.1.tgz", + "integrity": "sha512-76i3NpWf6bB8UHlVuLRxG4zW2YykF9CTEcq/9LGAiz2qBuX5cBStadkk0jSkg9a9TCIXbMQz7yzrygKoCW9JuA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-2.0.0.tgz", + "integrity": "sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.1.tgz", + "integrity": "sha512-XXbb1O/MW9HdEhnBxitZpPFbIvDgbo9NK4c/5bOfiKpnIGZDoL2xd7/e6jW5DYLsWxBbs+1nZEnVgnjnlFViaA==", + "dependencies": { + "cssnano-utils": "^4.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-5.0.1.tgz", + "integrity": "sha512-XzjBYKLd1t6vHsaokMV9URBt2EwC9a7nDhpQpjoPk2HRTSQfokPfyAS/Q7AOrzUu6q+vp/GnrDBGuj/FCaRqrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", "peerDependencies": { "postcss": "^8" } }, - "node_modules/postcss-normalize-charset": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", - "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-charset/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-normalize-charset/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", - "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", - "dependencies": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-display-values/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-normalize-display-values/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-display-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, - "node_modules/postcss-normalize-positions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", - "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", - "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-positions/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-normalize-positions/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-positions/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", - "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", - "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-repeat-style/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-normalize-repeat-style/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-repeat-style/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, - "node_modules/postcss-normalize-string": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", - "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", - "dependencies": { - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-string/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-normalize-string/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-string/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", - "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", - "dependencies": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-timing-functions/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-normalize-timing-functions/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-timing-functions/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, - "node_modules/postcss-normalize-unicode": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", - "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", - "dependencies": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-unicode/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-normalize-unicode/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-unicode/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, - "node_modules/postcss-normalize-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", - "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", - "dependencies": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-url/node_modules/normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/postcss-normalize-url/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-normalize-url/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-url/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, - "node_modules/postcss-normalize-whitespace": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", - "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", - "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-whitespace/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-normalize-whitespace/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-whitespace/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, - "node_modules/postcss-ordered-values": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", - "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", - "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-ordered-values/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-ordered-values/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-ordered-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, - "node_modules/postcss-overflow-shorthand": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz", - "integrity": "sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==", - "dependencies": { - "postcss": "^7.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-overflow-shorthand/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-overflow-shorthand/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-page-break": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-2.0.0.tgz", - "integrity": "sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==", - "dependencies": { - "postcss": "^7.0.2" - } - }, - "node_modules/postcss-page-break/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-page-break/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, "node_modules/postcss-place": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-4.0.1.tgz", - "integrity": "sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-9.0.1.tgz", + "integrity": "sha512-JfL+paQOgRQRMoYFc2f73pGuG/Aw3tt4vYMR6UA3cWVMxivviPTnMFnFTczUJOA4K2Zga6xgQVE+PcLs64WC8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-place/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-place/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-place/node_modules/postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "dependencies": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=6.14.4" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-preset-env": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.7.1.tgz", - "integrity": "sha512-rlRkgX9t0v2On33n7TK8pnkcYOATGQSv48J2RS8GsXhqtg+xk6AummHP88Y5mJo0TLJelBjePvSjScTNkj3+qw==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.3.0.tgz", + "integrity": "sha512-ycw6doPrqV6QxDCtgiyGDef61bEfiSc59HGM4gOw/wxQxmKnhuEery61oOC/5ViENz/ycpRsuhTexs1kUBTvVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "autoprefixer": "^9.6.1", - "browserslist": "^4.6.4", - "caniuse-lite": "^1.0.30000981", - "css-blank-pseudo": "^0.1.4", - "css-has-pseudo": "^0.10.0", - "css-prefers-color-scheme": "^3.1.1", - "cssdb": "^4.4.0", - "postcss": "^7.0.17", - "postcss-attribute-case-insensitive": "^4.0.1", - "postcss-color-functional-notation": "^2.0.1", - "postcss-color-gray": "^5.0.0", - "postcss-color-hex-alpha": "^5.0.3", - "postcss-color-mod-function": "^3.0.3", - "postcss-color-rebeccapurple": "^4.0.1", - "postcss-custom-media": "^7.0.8", - "postcss-custom-properties": "^8.0.11", - "postcss-custom-selectors": "^5.1.2", - "postcss-dir-pseudo-class": "^5.0.0", - "postcss-double-position-gradients": "^1.0.0", - "postcss-env-function": "^2.0.2", - "postcss-focus-visible": "^4.0.0", - "postcss-focus-within": "^3.0.0", - "postcss-font-variant": "^4.0.0", - "postcss-gap-properties": "^2.0.0", - "postcss-image-set-function": "^3.0.1", - "postcss-initial": "^3.0.0", - "postcss-lab-function": "^2.0.1", - "postcss-logical": "^3.0.0", - "postcss-media-minmax": "^4.0.0", - "postcss-nesting": "^7.0.0", - "postcss-overflow-shorthand": "^2.0.0", - "postcss-page-break": "^2.0.0", - "postcss-place": "^4.0.1", - "postcss-pseudo-class-any-link": "^6.0.0", - "postcss-replace-overflow-wrap": "^3.0.0", - "postcss-selector-matches": "^4.0.0", - "postcss-selector-not": "^4.0.0" + "@csstools/postcss-cascade-layers": "^4.0.1", + "@csstools/postcss-color-function": "^3.0.7", + "@csstools/postcss-color-mix-function": "^2.0.7", + "@csstools/postcss-exponential-functions": "^1.0.1", + "@csstools/postcss-font-format-keywords": "^3.0.0", + "@csstools/postcss-gamut-mapping": "^1.0.0", + "@csstools/postcss-gradients-interpolation-method": "^4.0.7", + "@csstools/postcss-hwb-function": "^3.0.6", + "@csstools/postcss-ic-unit": "^3.0.2", + "@csstools/postcss-initial": "^1.0.0", + "@csstools/postcss-is-pseudo-class": "^4.0.3", + "@csstools/postcss-logical-float-and-clear": "^2.0.0", + "@csstools/postcss-logical-overflow": "^1.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^1.0.0", + "@csstools/postcss-logical-resize": "^2.0.0", + "@csstools/postcss-logical-viewport-units": "^2.0.3", + "@csstools/postcss-media-minmax": "^1.1.0", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^2.0.3", + "@csstools/postcss-nested-calc": "^3.0.0", + "@csstools/postcss-normalize-display-values": "^3.0.1", + "@csstools/postcss-oklab-function": "^3.0.7", + "@csstools/postcss-progressive-custom-properties": "^3.0.2", + "@csstools/postcss-relative-color-syntax": "^2.0.7", + "@csstools/postcss-scope-pseudo-class": "^3.0.0", + "@csstools/postcss-stepped-value-functions": "^3.0.2", + "@csstools/postcss-text-decoration-shorthand": "^3.0.3", + "@csstools/postcss-trigonometric-functions": "^3.0.2", + "@csstools/postcss-unset-value": "^3.0.0", + "autoprefixer": "^10.4.16", + "browserslist": "^4.22.1", + "css-blank-pseudo": "^6.0.0", + "css-has-pseudo": "^6.0.0", + "css-prefers-color-scheme": "^9.0.0", + "cssdb": "^7.9.0", + "postcss-attribute-case-insensitive": "^6.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^6.0.2", + "postcss-color-hex-alpha": "^9.0.2", + "postcss-color-rebeccapurple": "^9.0.1", + "postcss-custom-media": "^10.0.2", + "postcss-custom-properties": "^13.3.2", + "postcss-custom-selectors": "^7.1.6", + "postcss-dir-pseudo-class": "^8.0.0", + "postcss-double-position-gradients": "^5.0.2", + "postcss-focus-visible": "^9.0.0", + "postcss-focus-within": "^8.0.0", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^5.0.0", + "postcss-image-set-function": "^6.0.1", + "postcss-lab-function": "^6.0.7", + "postcss-logical": "^7.0.0", + "postcss-nesting": "^12.0.1", + "postcss-opacity-percentage": "^2.0.0", + "postcss-overflow-shorthand": "^5.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^9.0.0", + "postcss-pseudo-class-any-link": "^9.0.0", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^7.0.1", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-preset-env/node_modules/autoprefixer": { - "version": "9.8.8", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", - "integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==", - "dependencies": { - "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001109", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "picocolors": "^0.2.1", - "postcss": "^7.0.32", - "postcss-value-parser": "^4.1.0" + "node": "^14 || ^16 || >=18" }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - }, - "node_modules/postcss-preset-env/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-preset-env/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-preset-env/node_modules/postcss-custom-properties": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz", - "integrity": "sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA==", - "dependencies": { - "postcss": "^7.0.17", - "postcss-values-parser": "^2.0.1" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-preset-env/node_modules/postcss-nesting": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-7.0.1.tgz", - "integrity": "sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg==", - "dependencies": { - "postcss": "^7.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-preset-env/node_modules/postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "dependencies": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=6.14.4" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-pseudo-class-any-link": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz", - "integrity": "sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-9.0.1.tgz", + "integrity": "sha512-cKYGGZ9yzUZi+dZd7XT2M8iSDfo+T2Ctbpiizf89uBTBfIpZpjvTavzIJXpCReMVXSKROqzpxClNu6fz4DHM0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss": "^7.0.2", - "postcss-selector-parser": "^5.0.0-rc.3" + "postcss-selector-parser": "^6.0.13" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-pseudo-class-any-link/node_modules/cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", - "bin": { - "cssesc": "bin/cssesc" + "node": "^14 || ^16 || >=18" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-pseudo-class-any-link/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-pseudo-class-any-link/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "dependencies": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=4" + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-reduce-initial": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", - "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.2.tgz", + "integrity": "sha512-YGKalhNlCLcjcLvjU5nF8FyeCTkCO5UtvJEt0hrPZVCTtRLSOH4z00T1UntQPj4dUmIYZgMj8qK77JbSX95hSw==", "dependencies": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0" + "browserslist": "^4.22.2", + "caniuse-api": "^3.0.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-reduce-initial/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-reduce-initial/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, "node_modules/postcss-reduce-transforms": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", - "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.1.tgz", + "integrity": "sha512-fUbV81OkUe75JM+VYO1gr/IoA2b/dRiH6HvMwhrIBSUrxq3jNZQZitSnugcTLDi1KkQh1eR/zi+iyxviUNBkcQ==", "dependencies": { - "cssnano-util-get-match": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-reduce-transforms/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-reduce-transforms/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/postcss-reduce-transforms/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/postcss-replace-overflow-wrap": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz", - "integrity": "sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==", - "dependencies": { - "postcss": "^7.0.2" - } - }, - "node_modules/postcss-replace-overflow-wrap/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-replace-overflow-wrap/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-selector-matches": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz", - "integrity": "sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==", - "dependencies": { - "balanced-match": "^1.0.0", - "postcss": "^7.0.2" - } - }, - "node_modules/postcss-selector-matches/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-selector-matches/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "peerDependencies": { + "postcss": "^8.0.3" } }, "node_modules/postcss-selector-not": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz", - "integrity": "sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-7.0.1.tgz", + "integrity": "sha512-1zT5C27b/zeJhchN7fP0kBr16Cc61mu7Si9uWWLoA3Px/D9tIJPKchJCkUH3tPO5D0pCFmGeApAv8XpXBQJ8SQ==", "dependencies": { - "balanced-match": "^1.0.0", - "postcss": "^7.0.2" - } - }, - "node_modules/postcss-selector-not/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-selector-not/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "postcss-selector-parser": "^6.0.10" }, "engines": { - "node": ">=6.0.0" + "node": "^14 || ^16 || >=18" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -13003,83 +12778,38 @@ } }, "node_modules/postcss-svgo": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz", - "integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.2.tgz", + "integrity": "sha512-IH5R9SjkTkh0kfFOQDImyy1+mTCb+E830+9SV1O+AaDcoHTvfsvt6WwJeo7KwcHbFnevZVCsXhDmjFiGVuwqFQ==", "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "svgo": "^1.0.0" + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-svgo/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-svgo/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >= 18" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/postcss-svgo/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/postcss-unique-selectors": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", - "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.2.tgz", + "integrity": "sha512-8IZGQ94nechdG7Y9Sh9FlIY2b4uS8/k8kdKRX040XHsS3B6d1HrJAkXrBSsSu4SuARruSsUjW3nlSw8BHkaAYQ==", "dependencies": { - "alphanum-sort": "^1.0.0", - "postcss": "^7.0.0", - "uniqs": "^2.0.0" + "postcss-selector-parser": "^6.0.15" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-unique-selectors/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss-unique-selectors/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "node": "^14 || ^16 || >=18.0" }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "postcss": "^8.4.31" } }, "node_modules/postcss-url": { "version": "10.1.3", "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-10.1.3.tgz", "integrity": "sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==", - "dev": true, "dependencies": { "make-dir": "~3.1.0", "mime": "~2.5.2", @@ -13093,11 +12823,19 @@ "postcss": "^8.0.0" } }, + "node_modules/postcss-url/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/postcss-url/node_modules/minimatch": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -13110,43 +12848,6 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, - "node_modules/postcss-values-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-4.0.0.tgz", - "integrity": "sha512-R9x2D87FcbhwXUmoCXJR85M1BLII5suXRuXibGYyBJ7lVDEpRIdKZh4+8q5S+/+A4m0IoG1U5tFw39asyhX/Hw==", - "dev": true, - "dependencies": { - "color-name": "^1.1.4", - "is-url-superb": "^4.0.0", - "postcss": "^7.0.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/postcss-values-parser/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-values-parser/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, "node_modules/prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", @@ -13156,9 +12857,9 @@ } }, "node_modules/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "optional": true, "bin": { "prettier": "bin-prettier.js" @@ -13190,15 +12891,6 @@ "renderkid": "^2.0.4" } }, - "node_modules/pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/pretty-time": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", @@ -13297,44 +12989,25 @@ } }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "engines": { "node": ">=6" } }, - "node_modules/purgecss": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.1.3.tgz", - "integrity": "sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==", - "dev": true, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dependencies": { - "commander": "^8.0.0", - "glob": "^7.1.7", - "postcss": "^8.3.5", - "postcss-selector-parser": "^6.0.6" + "side-channel": "^1.0.4" }, - "bin": { - "purgecss": "bin/purgecss.js" - } - }, - "node_modules/purgecss/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, "engines": { - "node": ">= 12" - } - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/query-string": { @@ -13349,15 +13022,6 @@ "node": ">=0.10.0" } }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/querystring-es3": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", @@ -13385,18 +13049,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -13441,20 +13093,44 @@ "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/rc9": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-1.2.4.tgz", - "integrity": "sha512-YD1oJO9LUzMdmr2sAsVlwQVtEoDCmvuyDwmSWrg2GKFprl3BckP5cmw9rHPunei0lV6Xl4E5t2esT+0trY1xfQ==", + "node_modules/raw-loader/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/raw-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dependencies": { - "defu": "^6.0.0", - "destr": "^1.1.1", - "flat": "^5.0.0" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/rc9": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.1.tgz", + "integrity": "sha512-lNeOl38Ws0eNxpO3+wD1I9rkHGQyj1NU1jlzv4go2CtEnEQEUfqnIvZG7W+bC/aXdJ27n5x/yUjb6RoT9tko+Q==", + "dependencies": { + "defu": "^6.1.2", + "destr": "^2.0.0", + "flat": "^5.0.2" } }, "node_modules/rc9/node_modules/defu": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.1.tgz", - "integrity": "sha512-aA964RUCsBt0FGoNIlA3uFgo2hO+WWC0fiC6DBps/0SFzkKcYoM/3CzVLIa5xSsrFjdioMdYgAIbwo80qp2MoA==" + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" }, "node_modules/read-cache": { "version": "1.0.0", @@ -13473,9 +13149,9 @@ } }, "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -13497,31 +13173,15 @@ "node": ">=8.10.0" } }, - "node_modules/reduce-css-calc": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", - "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", - "dev": true, - "dependencies": { - "css-unit-converter": "^1.1.1", - "postcss-value-parser": "^3.3.0" - } - }, - "node_modules/reduce-css-calc/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", "dependencies": { "regenerate": "^1.4.2" }, @@ -13530,14 +13190,14 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", "dependencies": { "@babel/runtime": "^7.8.4" } @@ -13554,37 +13214,14 @@ "node": ">=0.10.0" } }, - "node_modules/regex-not/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regex-not/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" }, "engines": { "node": ">= 0.4" @@ -13594,13 +13231,13 @@ } }, "node_modules/regexpu-core": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", - "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", "dependencies": { + "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.1.0", - "regjsgen": "^0.7.1", "regjsparser": "^0.9.1", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" @@ -13609,11 +13246,6 @@ "node": ">=4" } }, - "node_modules/regjsgen": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", - "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==" - }, "node_modules/regjsparser": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", @@ -13667,6 +13299,69 @@ "node": ">=0.10.0" } }, + "node_modules/renderkid/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/renderkid/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -13694,28 +13389,10 @@ "node": ">=0.10" } }, - "node_modules/replace-in-file": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.5.tgz", - "integrity": "sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.2", - "glob": "^7.2.0", - "yargs": "^17.2.1" - }, - "bin": { - "replace-in-file": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "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, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "engines": { "node": ">=0.10.0" } @@ -13726,11 +13403,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -13741,64 +13418,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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-path": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", - "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", - "dev": true, - "dependencies": { - "http-errors": "~1.6.2", - "path-is-absolute": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/resolve-path/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/resolve-path/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/resolve-path/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true - }, - "node_modules/resolve-path/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -13842,16 +13461,6 @@ "node": ">=0.10.0" } }, - "node_modules/rgb-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w==" - }, - "node_modules/rgba-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==" - }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -13863,6 +13472,45 @@ "rimraf": "bin.js" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", @@ -13921,6 +13569,28 @@ "npm": ">=2.0.0" } }, + "node_modules/safe-array-concat": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", + "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -13935,14 +13605,17 @@ } }, "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", + "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", "is-regex": "^1.1.4" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -13952,51 +13625,10 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/sass-loader": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.1.1.tgz", - "integrity": "sha512-W6gVDXAd5hR/WHsPicvZdjAWHBcEJ44UahgxcIE196fW2ong0ZHMPO1kZuI5q0VlvMQZh32gpv69PLWQm70qrw==", - "dependencies": { - "klona": "^2.0.4", - "loader-utils": "^2.0.0", - "neo-async": "^2.6.2", - "schema-utils": "^3.0.0", - "semver": "^7.3.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0", - "sass": "^1.3.0", - "webpack": "^4.36.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, "node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -14016,9 +13648,9 @@ "integrity": "sha512-M9gnWtn3J0W+UhJOHmBxBTwv8mZCan5i1Himp60t6vvZcor0wr+IM0URKmIglsWJ7bRujNAVVN77fp+uZaWoKg==" }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "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" }, @@ -14117,25 +13749,25 @@ } }, "node_modules/serialize-javascript": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", - "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dependencies": { "randombytes": "^2.1.0" } }, "node_modules/serve-placeholder": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/serve-placeholder/-/serve-placeholder-1.2.4.tgz", - "integrity": "sha512-jWD9cZXLcr4vHTTL5KEPIUBUYyOWN/z6v/tn0l6XxFhi9iqV3Fc5Y1aFeduUyz+cx8sALzGCUczkPfeOlrq9jg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/serve-placeholder/-/serve-placeholder-2.0.1.tgz", + "integrity": "sha512-rUzLlXk4uPFnbEaIz3SW8VISTxMuONas88nYWjAWaM2W9VDbt9tyFOr3lq8RhVOFrT3XISoBw8vni5una8qMnQ==", "dependencies": { - "defu": "^5.0.0" + "defu": "^6.0.0" } }, "node_modules/serve-placeholder/node_modules/defu": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-5.0.1.tgz", - "integrity": "sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==" + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" }, "node_modules/serve-static": { "version": "1.15.0", @@ -14156,6 +13788,34 @@ "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==" }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -14170,6 +13830,25 @@ "node": ">=0.10.0" } }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -14224,9 +13903,9 @@ } }, "node_modules/shell-quote": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", - "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -14249,27 +13928,14 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, "node_modules/sirv": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", - "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", "dependencies": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^1.0.0" + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" }, "engines": { "node": ">= 10" @@ -14325,41 +13991,6 @@ "node": ">=0.10.0" } }, - "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/snapdragon-util": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", @@ -14390,6 +14021,48 @@ "ms": "2.0.0" } }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/snapdragon/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -14404,44 +14077,48 @@ } }, "node_modules/socket.io": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz", - "integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", + "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", + "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.2.0", - "socket.io-adapter": "~2.4.0", - "socket.io-parser": "~4.2.0" + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/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==" + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dependencies": { + "ws": "~8.11.0" + } }, "node_modules/socket.io-client": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.3.tgz", - "integrity": "sha512-I/hqDYpQ6JKwtJOf5ikM+Qz+YujZPMEl6qBLhxiP0nX+TfXKhW4KZZG8lamrD6Y5ngjmYHreESVasVCgi5Kl3A==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz", + "integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", - "engine.io-client": "~6.2.3", - "socket.io-parser": "~4.2.0" + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" }, "engines": { "node": ">=10.0.0" } }, "node_modules/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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -14534,34 +14211,6 @@ "node": ">=0.10.0" } }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.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==" - }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -14604,6 +14253,29 @@ "node": ">=0.10.0" } }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -14613,12 +14285,9 @@ } }, "node_modules/std-env": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-2.3.1.tgz", - "integrity": "sha512-eOsoKTWnr6C8aWrqJJ2KAReXoa7Vn5Ywyw6uCXgA/xDhxPoaIsBa5aNJmISY04dLwXPBnDHW4diGM7Sn5K4R/g==", - "dependencies": { - "ci-info": "^3.1.1" - } + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==" }, "node_modules/stream-browserify": { "version": "2.0.2", @@ -14651,9 +14320,9 @@ } }, "node_modules/stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" }, "node_modules/strict-uri-encode": { "version": "1.1.0", @@ -14684,27 +14353,58 @@ "node": ">=8" } }, - "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "node_modules/string-width-cjs": { + "name": "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==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14721,6 +14421,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "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", @@ -14746,6 +14459,69 @@ "webpack": "^3.0.0 || ^4.0.0 || ^5.0.0" } }, + "node_modules/style-resources-loader/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/style-resources-loader/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/style-resources-loader/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/style-resources-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/style-resources-loader/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/style-resources-loader/node_modules/schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -14764,55 +14540,100 @@ } }, "node_modules/style-resources-loader/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/stylehacks": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", - "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.0.2.tgz", + "integrity": "sha512-00zvJGnCu64EpMjX8b5iCZ3us2Ptyw8+toEkb92VdmkEaRaSGBNKAoK6aWZckhXxmQP8zWiTaFaiMGIU8Ve8sg==", "dependencies": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" + "browserslist": "^4.22.2", + "postcss-selector-parser": "^6.0.15" }, "engines": { - "node": ">=6.9.0" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/stylehacks/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/stylehacks/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" }, "engines": { - "node": ">=6.0.0" + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/stylehacks/node_modules/postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, "dependencies": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/supports-color": { @@ -14843,215 +14664,72 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==" }, "node_modules/svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", + "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==", "dependencies": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" }, "bin": { "svgo": "bin/svgo" }, "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/svgo/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==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/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==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/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==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/svgo/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==" - }, - "node_modules/svgo/node_modules/css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "node_modules/svgo/node_modules/css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "engines": { - "node": ">= 6" + "node": ">=14.0.0" }, "funding": { - "url": "https://github.com/sponsors/fb55" + "type": "opencollective", + "url": "https://opencollective.com/svgo" } }, - "node_modules/svgo/node_modules/dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/svgo/node_modules/domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" - }, - "node_modules/svgo/node_modules/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==", + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dependencies": { - "boolbase": "~1.0.0" - } - }, - "node_modules/svgo/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tailwind-config-viewer": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/tailwind-config-viewer/-/tailwind-config-viewer-1.7.2.tgz", - "integrity": "sha512-3JJCeAAlvG+i/EBj+tQb0x4weo30QjdSAo4hlcnVbtD+CkpzHi/UwU9InbPMcYH+ESActoa2kCyjpLEyjEkn0Q==", - "dev": true, - "dependencies": { - "@koa/router": "^9.0.1", - "commander": "^6.0.0", - "fs-extra": "^9.0.1", - "koa": "^2.12.0", - "koa-static": "^5.0.0", - "open": "^7.0.4", - "portfinder": "^1.0.26", - "replace-in-file": "^6.1.0" - }, - "bin": { - "tailwind-config-viewer": "cli/index.js", - "tailwindcss-config-viewer": "cli/index.js" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "tailwindcss": "1 || 2 || 2.0.1-compat || 3" - } - }, - "node_modules/tailwind-config-viewer/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "engines": { - "node": ">= 6" + "node": ">= 10" } }, "node_modules/tailwindcss": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz", - "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", "dev": true, "dependencies": { + "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", - "color-name": "^1.1.4", - "detective": "^5.2.1", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.12", + "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "lilconfig": "^2.0.6", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.4.18", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", - "postcss-selector-parser": "^6.0.10", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1" + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" }, "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "postcss": "^8.0.9" + "node": ">=14.0.0" } }, "node_modules/tailwindcss/node_modules/glob-parent": { @@ -15066,10 +14744,19 @@ "node": ">=10.13.0" } }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/tailwindcss/node_modules/postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", @@ -15077,7 +14764,7 @@ "resolve": "^1.1.7" }, "engines": { - "node": ">=10.0.0" + "node": ">=14.0.0" }, "peerDependencies": { "postcss": "^8.0.0" @@ -15092,13 +14779,13 @@ } }, "node_modules/tar": { - "version": "6.1.12", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.12.tgz", - "integrity": "sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" @@ -15107,6 +14794,14 @@ "node": ">=10" } }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/tar/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -15166,9 +14861,9 @@ } }, "node_modules/terser-webpack-plugin/node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "bin": { "acorn": "bin/acorn" }, @@ -15190,13 +14885,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/terser-webpack-plugin/node_modules/terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", + "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "randombytes": "^2.1.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -15207,10 +14910,26 @@ "node": ">=10" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } }, "node_modules/thread-loader": { "version": "3.0.4", @@ -15234,6 +14953,30 @@ "webpack": "^4.27.0 || ^5.0.0" } }, + "node_modules/thread-loader/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -15267,11 +15010,6 @@ "node": ">=0.6.0" } }, - "node_modules/timsort": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==" - }, "node_modules/tiny-emitter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", @@ -15348,76 +15086,6 @@ "node": ">=8.0" } }, - "node_modules/to-regex/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -15427,9 +15095,9 @@ } }, "node_modules/totalist": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", - "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "engines": { "node": ">=6" } @@ -15444,6 +15112,12 @@ "resolved": "https://registry.npmjs.org/trix/-/trix-1.3.1.tgz", "integrity": "sha512-BbH6mb6gk+AV4f2as38mP6Ucc1LE3OD6XxkZnAgPIduWXYtvg2mI3cZhIZSLqmMh9OITEpOBCCk88IVmyjU7bA==" }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, "node_modules/ts-pnp": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", @@ -15462,15 +15136,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, - "node_modules/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "dev": true, - "engines": { - "node": ">=0.6.x" - } - }, "node_modules/tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -15490,17 +15155,65 @@ "node": ">=8" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/typedarray": { @@ -15509,9 +15222,9 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/ua-parser-js": { - "version": "0.7.32", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz", - "integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==", + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", "funding": [ { "type": "opencollective", @@ -15520,6 +15233,10 @@ { "type": "paypal", "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" } ], "engines": { @@ -15527,9 +15244,9 @@ } }, "node_modules/ufo": { - "version": "0.7.11", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-0.7.11.tgz", - "integrity": "sha512-IT3q0lPvtkqQ8toHQN/BkOi4VIqoqheqM1FnkNWT9y0G8B3xJhwnoKBu5OHx8zHDOvveQzfKuFowJ0VSARiIDg==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", + "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==" }, "node_modules/uglify-js": { "version": "3.17.4", @@ -15556,10 +15273,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unfetch": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", - "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-5.0.0.tgz", + "integrity": "sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -15611,15 +15333,13 @@ "node": ">=0.10.0" } }, - "node_modules/uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==" - }, - "node_modules/uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ==" + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/unique-filename": { "version": "1.1.1", @@ -15638,9 +15358,9 @@ } }, "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "engines": { "node": ">= 10.0.0" } @@ -15653,11 +15373,6 @@ "node": ">= 0.8" } }, - "node_modules/unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==" - }, "node_modules/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -15712,9 +15427,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "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==", "funding": [ { "type": "opencollective", @@ -15723,6 +15438,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { @@ -15730,7 +15449,7 @@ "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -15756,12 +15475,12 @@ "deprecated": "Please see https://github.com/lydell/urix#deprecated" }, "node_modules/url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" + "punycode": "^1.4.1", + "qs": "^6.11.2" } }, "node_modules/url-loader": { @@ -15790,10 +15509,34 @@ } } }, + "node_modules/url-loader/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" }, "node_modules/use": { "version": "3.1.1", @@ -15859,26 +15602,18 @@ "node": ">= 0.8" } }, - "node_modules/vendors": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", - "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, "node_modules/vue": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz", - "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", "dependencies": { - "@vue/compiler-sfc": "2.7.14", + "@vue/compiler-sfc": "2.7.16", "csstype": "^3.1.0" } }, @@ -15893,9 +15628,9 @@ "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==" }, "node_modules/vue-loader": { - "version": "15.10.1", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz", - "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==", + "version": "15.11.1", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.11.1.tgz", + "integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==", "dependencies": { "@vue/component-compiler-utils": "^3.1.0", "hash-sum": "^1.0.2", @@ -15911,6 +15646,9 @@ "cache-loader": { "optional": true }, + "prettier": { + "optional": true + }, "vue-template-compiler": { "optional": true } @@ -15921,30 +15659,6 @@ "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==" }, - "node_modules/vue-loader/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/vue-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/vue-meta": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/vue-meta/-/vue-meta-2.4.0.tgz", @@ -15969,9 +15683,9 @@ "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==" }, "node_modules/vue-server-renderer": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-server-renderer/-/vue-server-renderer-2.7.14.tgz", - "integrity": "sha512-NlGFn24tnUrj7Sqb8njhIhWREuCJcM3140aMunLNcx951BHG8j3XOrPP7psSCaFA8z6L4IWEjudztdwTp1CBVw==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-server-renderer/-/vue-server-renderer-2.7.16.tgz", + "integrity": "sha512-U7GgR4rYmHmbs3Z2gqsasfk7JNuTsy/xrR5EMMGRLkjN8+ryDlqQq6Uu3DcmbCATAei814YOxyl0eq2HNqgXyQ==", "dependencies": { "chalk": "^4.1.2", "hash-sum": "^2.0.0", @@ -15983,14 +15697,6 @@ "source-map": "0.5.6" } }, - "node_modules/vue-server-renderer/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==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/vue-server-renderer/node_modules/source-map": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", @@ -16013,34 +15719,10 @@ "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==" }, - "node_modules/vue-style-loader/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/vue-style-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/vue-template-compiler": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", - "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" @@ -16076,16 +15758,15 @@ } }, "node_modules/watchpack": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", - "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", "dependencies": { - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" }, - "optionalDependencies": { - "chokidar": "^3.4.1", - "watchpack-chokidar2": "^2.0.1" + "engines": { + "node": ">=10.13.0" } }, "node_modules/watchpack-chokidar2": { @@ -16149,6 +15830,18 @@ "node": ">=0.10.0" } }, + "node_modules/watchpack-chokidar2/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "optional": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/watchpack-chokidar2/node_modules/chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -16172,19 +15865,6 @@ "fsevents": "^1.2.7" } }, - "node_modules/watchpack-chokidar2/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "optional": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/watchpack-chokidar2/node_modules/fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -16200,11 +15880,23 @@ "node": ">=0.10.0" } }, + "node_modules/watchpack-chokidar2/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "optional": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/watchpack-chokidar2/node_modules/fsevents": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2", "hasInstallScript": true, "optional": true, "os": [ @@ -16240,18 +15932,6 @@ "node": ">=0.10.0" } }, - "node_modules/watchpack-chokidar2/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "optional": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/watchpack-chokidar2/node_modules/is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -16264,40 +15944,11 @@ "node": ">=0.10.0" } }, - "node_modules/watchpack-chokidar2/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "optional": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "optional": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/watchpack-chokidar2/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "optional": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, "engines": { "node": ">=0.10.0" } @@ -16350,19 +16001,6 @@ "node": ">=0.10.0" } }, - "node_modules/watchpack-chokidar2/node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "optional": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/watchpack-chokidar2/node_modules/readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", @@ -16406,9 +16044,9 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "4.46.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", - "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", + "version": "4.47.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz", + "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", "dependencies": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", @@ -16454,18 +16092,22 @@ } }, "node_modules/webpack-bundle-analyzer": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz", - "integrity": "sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", + "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==", "dependencies": { + "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", - "lodash": "^4.17.20", + "html-escaper": "^2.0.2", + "is-plain-object": "^5.0.0", "opener": "^1.5.2", - "sirv": "^1.0.7", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { @@ -16476,9 +16118,9 @@ } }, "node_modules/webpack-bundle-analyzer/node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "bin": { "acorn": "bin/acorn" }, @@ -16486,14 +16128,6 @@ "node": ">=0.4.0" } }, - "node_modules/webpack-bundle-analyzer/node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/webpack-bundle-analyzer/node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -16502,6 +16136,25 @@ "node": ">= 10" } }, + "node_modules/webpack-bundle-analyzer/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==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/webpack-bundle-analyzer/node_modules/ws": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", @@ -16523,19 +16176,18 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz", - "integrity": "sha512-PjwyVY95/bhBh6VUqt6z4THplYcsvQ8YNNBTBM873xLVmw8FLeALn0qurHbs9EmcfhzQis/eoqypSnZeuUz26w==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", "dependencies": { - "colorette": "^1.2.2", - "mem": "^8.1.1", - "memfs": "^3.2.2", - "mime-types": "^2.1.30", + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", "range-parser": "^1.2.1", - "schema-utils": "^3.0.0" + "schema-utils": "^4.0.0" }, "engines": { - "node": ">= v10.23.3" + "node": ">= 12.13.0" }, "funding": { "type": "opencollective", @@ -16545,10 +16197,59 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/webpack-hot-middleware": { - "version": "2.25.3", - "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.25.3.tgz", - "integrity": "sha512-IK/0WAHs7MTu1tzLTjio73LjS3Ov+VvBKQmE8WPlJutgG5zT6Urgq/BbAdRrHTRpyzK0dvAvFh1Qg98akxgZpA==", + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.0.tgz", + "integrity": "sha512-okzjec5sAEy4t+7rzdT8eRyxsk0FDSmBPN2KwX4Qd+6+oQCfe5Ve07+u7cJvofgB+B4w5/4dO4Pz0jhhHyyPLQ==", "dependencies": { "ansi-html-community": "0.0.8", "html-entities": "^2.1.0", @@ -16572,15 +16273,13 @@ "source-map": "~0.6.1" } }, - "node_modules/webpack/node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "node_modules/webpack/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/webpack/node_modules/braces": { @@ -16603,6 +16302,17 @@ "node": ">=0.10.0" } }, + "node_modules/webpack/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/webpack/node_modules/cacache": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", @@ -16630,18 +16340,6 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, - "node_modules/webpack/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/webpack/node_modules/fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -16656,6 +16354,17 @@ "node": ">=0.10.0" } }, + "node_modules/webpack/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/webpack/node_modules/find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -16680,48 +16389,29 @@ "node": ">=6" } }, - "node_modules/webpack/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "node_modules/webpack/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dependencies": { - "kind-of": "^6.0.0" + "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" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dependencies": { - "kind-of": "^6.0.0" + "node": "*" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/webpack/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dependencies": { - "is-plain-object": "^2.0.4" - }, + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "engines": { "node": ">=0.10.0" } @@ -16756,17 +16446,6 @@ "node": ">=4" } }, - "node_modules/webpack/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/webpack/node_modules/loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", @@ -16775,19 +16454,6 @@ "node": ">=4.3.0 <5.0.0 || >=5.10" } }, - "node_modules/webpack/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/webpack/node_modules/locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -16844,16 +16510,15 @@ "node": ">=0.10.0" } }, - "node_modules/webpack/node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "node_modules/webpack/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=0.10.0" + "node": "*" } }, "node_modules/webpack/node_modules/p-locate": { @@ -16908,9 +16573,9 @@ } }, "node_modules/webpack/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -16965,99 +16630,51 @@ "node": ">=0.10.0" } }, + "node_modules/webpack/node_modules/watchpack": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", + "dependencies": { + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + }, + "optionalDependencies": { + "chokidar": "^3.4.1", + "watchpack-chokidar2": "^2.0.1" + } + }, "node_modules/webpack/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==" }, "node_modules/webpackbar": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-4.0.0.tgz", - "integrity": "sha512-k1qRoSL/3BVuINzngj09nIwreD8wxV4grcuhHTD8VJgUbGcy8lQSPqv+bM00B7F+PffwIsQ8ISd4mIwRbr23eQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.0.tgz", + "integrity": "sha512-RdB0RskzOaix1VFMnBXSkKMbUgvZliRqgoNp0gCnG6iUe9RS9sf018AJ/1h5NAeh+ttwXkXjXKC6NdjE/OOcaA==", "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", - "consola": "^2.10.0", - "figures": "^3.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "consola": "^3.2.3", + "figures": "^3.2.0", + "markdown-table": "^2.0.0", "pretty-time": "^1.1.0", - "std-env": "^2.2.1", - "text-table": "^0.2.0", - "wrap-ansi": "^6.0.0" + "std-env": "^3.6.0", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">= 8.0.0" + "node": ">=14.21.3" }, "peerDependencies": { - "webpack": "^3.0.0 || ^4.0.0" + "webpack": "3 || 4 || 5" } }, - "node_modules/webpackbar/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==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, + "node_modules/webpackbar/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", "engines": { - "node": ">=4" - } - }, - "node_modules/webpackbar/node_modules/chalk/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==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/webpackbar/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==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/webpackbar/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==" - }, - "node_modules/webpackbar/node_modules/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==", - "engines": { - "node": ">=4" - } - }, - "node_modules/webpackbar/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/webpackbar/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==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" + "node": "^14.18.0 || >=16.10.0" } }, "node_modules/whatwg-url": { @@ -17098,6 +16715,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", @@ -17124,54 +16759,49 @@ } }, "node_modules/worker-loader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz", - "integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", + "peer": true, "dependencies": { - "loader-utils": "^1.0.0", - "schema-utils": "^0.4.0" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" }, "engines": { - "node": ">= 6.9.0 || >= 8.9.0" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^3.0.0 || ^4.0.0-alpha.0 || ^4.0.0" + "webpack": "^4.0.0 || ^5.0.0" } }, "node_modules/worker-loader/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dependencies": { - "minimist": "^1.2.0" - }, + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "peer": true, "bin": { "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/worker-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "peer": true, "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "json5": "^2.1.2" }, "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/worker-loader/node_modules/schema-utils": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", - "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", - "dependencies": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" + "node": ">=8.9.0" } }, "node_modules/wrap-ansi": { @@ -17190,6 +16820,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "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", @@ -17260,9 +16908,9 @@ } }, "node_modules/ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "engines": { "node": ">=10.0.0" }, @@ -17303,15 +16951,6 @@ "cuint": "^0.2.2" } }, - "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": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -17321,47 +16960,10 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, "engines": { "node": ">= 6" } }, - "node_modules/yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", - "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/ylru": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.3.2.tgz", - "integrity": "sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -17373,13384 +16975,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "requires": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "requires": { - "@babel/highlight": "^7.18.6" - } - }, - "@babel/compat-data": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", - "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==" - }, - "@babel/core": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz", - "integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==", - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.2", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-module-transforms": "^7.20.2", - "@babel/helpers": "^7.20.1", - "@babel/parser": "^7.20.2", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "@babel/generator": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz", - "integrity": "sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA==", - "requires": { - "@babel/types": "^7.20.2", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" - }, - "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - } - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", - "requires": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", - "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", - "requires": { - "@babel/compat-data": "^7.20.0", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.2.tgz", - "integrity": "sha512-k22GoYRAHPYr9I+Gvy2ZQlAe5mGy8BqWst2wRt8cwIufWTxrsVshhIBvYNqC80N0GSFWTsqRVexOtfzlgOEDvA==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz", - "integrity": "sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.1.0" - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", - "requires": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", - "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", - "requires": { - "@babel/types": "^7.18.9" - } - }, - "@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-module-transforms": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", - "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==" - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" - } - }, - "@babel/helper-replace-supers": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz", - "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==", - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/traverse": "^7.19.1", - "@babel/types": "^7.19.0" - } - }, - "@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", - "requires": { - "@babel/types": "^7.20.2" - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", - "requires": { - "@babel/types": "^7.20.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" - }, - "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" - }, - "@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==" - }, - "@babel/helper-wrap-function": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz", - "integrity": "sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==", - "requires": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" - } - }, - "@babel/helpers": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", - "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", - "requires": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.0" - } - }, - "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "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==", - "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==", - "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==", - "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==" - }, - "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==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.3.tgz", - "integrity": "sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg==" - }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", - "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-proposal-optional-chaining": "^7.18.9" - } - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz", - "integrity": "sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==", - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-proposal-class-static-block": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", - "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - } - }, - "@babel/plugin-proposal-decorators": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.2.tgz", - "integrity": "sha512-nkBH96IBmgKnbHQ5gXFrcmez+Z9S2EIDKDQGp005ROqBigc88Tky4rzCnlP/lnlj245dCEQl4/YyV0V1kYh5dw==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.20.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/plugin-syntax-decorators": "^7.19.0" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", - "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - } - }, - "@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz", - "integrity": "sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==", - "requires": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.1" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", - "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", - "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@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==", - "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==", - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-decorators": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz", - "integrity": "sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@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==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@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==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", - "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", - "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", - "requires": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.2.tgz", - "integrity": "sha512-y5V15+04ry69OV2wULmwhEA6jwSWXO1TwAtIwiPXcvHcoOQUqpyMVd2bDsQJMW8AurjulIyUV8kDqtjSwHy1uQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.2.tgz", - "integrity": "sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", - "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz", - "integrity": "sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", - "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", - "requires": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz", - "integrity": "sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==", - "requires": { - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz", - "integrity": "sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==", - "requires": { - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-simple-access": "^7.19.4" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz", - "integrity": "sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==", - "requires": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-identifier": "^7.19.1" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", - "requires": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz", - "integrity": "sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.3.tgz", - "integrity": "sha512-oZg/Fpx0YDrj13KsLyO8I/CX3Zdw7z0O9qOd95SqcoIzuqy/WTGWvePeHAnZCN54SfdyjHcb1S30gc8zlzlHcA==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", - "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "regenerator-transform": "^0.15.0" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-runtime": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", - "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", - "requires": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.19.0", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz", - "integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==", - "requires": { - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/preset-env": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", - "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", - "requires": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.20.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@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-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.20.2", - "@babel/plugin-transform-classes": "^7.20.2", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.20.2", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.19.6", - "@babel/plugin-transform-modules-commonjs": "^7.19.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.6", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.20.1", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/runtime": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz", - "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==", - "requires": { - "regenerator-runtime": "^0.13.10" - } - }, - "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" - } - }, - "@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz", - "integrity": "sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==", - "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - } - }, - "@csstools/convert-colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", - "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" - }, - "@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==" - }, - "@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "requires": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" - }, - "@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==" - }, - "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - } - } - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "@koa/router": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-9.4.0.tgz", - "integrity": "sha512-dOOXgzqaDoHu5qqMEPLKEgLz5CeIA7q8+1W62mCvFVCOqeC71UoTGJ4u1xUSOpIl2J1x2pqrNULkFteUeZW3/A==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "http-errors": "^1.7.3", - "koa-compose": "^4.1.0", - "methods": "^1.1.2", - "path-to-regexp": "^6.1.0" - }, - "dependencies": { - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true - }, - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - } - } - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.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==", - "requires": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "@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==", - "requires": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "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" - } - } - } - }, - "@nuxt/babel-preset-app": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/babel-preset-app/-/babel-preset-app-2.15.8.tgz", - "integrity": "sha512-z23bY5P7dLTmIbk0ZZ95mcEXIEER/mQCOqEp2vxnzG2nurks+vq6tNcUAXqME1Wl6aXWTXlqky5plBe7RQHzhQ==", - "requires": { - "@babel/compat-data": "^7.14.0", - "@babel/core": "^7.14.0", - "@babel/helper-compilation-targets": "^7.13.16", - "@babel/helper-module-imports": "^7.13.12", - "@babel/plugin-proposal-class-properties": "^7.13.0", - "@babel/plugin-proposal-decorators": "^7.13.15", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", - "@babel/plugin-proposal-optional-chaining": "^7.13.12", - "@babel/plugin-proposal-private-methods": "^7.13.0", - "@babel/plugin-transform-runtime": "^7.13.15", - "@babel/preset-env": "^7.14.1", - "@babel/runtime": "^7.14.0", - "@vue/babel-preset-jsx": "^1.2.4", - "core-js": "^2.6.5", - "core-js-compat": "^3.12.1", - "regenerator-runtime": "^0.13.7" - }, - "dependencies": { - "core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" - } - } - }, - "@nuxt/builder": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/builder/-/builder-2.15.8.tgz", - "integrity": "sha512-WVhN874LFMdgRiJqpxmeKI+vh5lhCUBVOyR9PhL1m1V/GV3fb+Dqc1BKS6XgayrWAWavPLveCJmQ/FID0puOfQ==", - "requires": { - "@nuxt/devalue": "^1.2.5", - "@nuxt/utils": "2.15.8", - "@nuxt/vue-app": "2.15.8", - "@nuxt/webpack": "2.15.8", - "chalk": "^4.1.1", - "chokidar": "^3.5.1", - "consola": "^2.15.3", - "fs-extra": "^9.1.0", - "glob": "^7.1.7", - "hash-sum": "^2.0.0", - "ignore": "^5.1.8", - "lodash": "^4.17.21", - "pify": "^5.0.0", - "serialize-javascript": "^5.0.1", - "upath": "^2.0.1" - } - }, - "@nuxt/cli": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/cli/-/cli-2.15.8.tgz", - "integrity": "sha512-KcGIILW/dAjBKea1DHsuLCG1sNzhzETShwT23DhXWO304qL8ljf4ndYKzn2RenzauGRGz7MREta80CbJCkLSHw==", - "requires": { - "@nuxt/config": "2.15.8", - "@nuxt/utils": "2.15.8", - "boxen": "^5.0.1", - "chalk": "^4.1.1", - "compression": "^1.7.4", - "connect": "^3.7.0", - "consola": "^2.15.3", - "crc": "^3.8.0", - "defu": "^4.0.1", - "destr": "^1.1.0", - "execa": "^5.0.0", - "exit": "^0.1.2", - "fs-extra": "^9.1.0", - "globby": "^11.0.3", - "hable": "^3.0.0", - "lodash": "^4.17.21", - "minimist": "^1.2.5", - "opener": "1.5.2", - "pretty-bytes": "^5.6.0", - "semver": "^7.3.5", - "serve-static": "^1.14.1", - "std-env": "^2.3.0", - "upath": "^2.0.1", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "defu": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-4.0.1.tgz", - "integrity": "sha512-lC+G0KvvWRbisQa50+iFelm3/eMmwo4IlBmfASOVlw9MZpHHyQeVsZxc5j23+TQy5ydgEoTVSrWl7ptou1kzJQ==" - } - } - }, - "@nuxt/components": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@nuxt/components/-/components-2.2.1.tgz", - "integrity": "sha512-r1LHUzifvheTnJtYrMuA+apgsrEJbxcgFKIimeXKb+jl8TnPWdV3egmrxBCaDJchrtY/wmHyP47tunsft7AWwg==", - "requires": { - "chalk": "^4.1.2", - "chokidar": "^3.5.2", - "glob": "^7.1.7", - "globby": "^11.0.4", - "scule": "^0.2.1", - "semver": "^7.3.5", - "upath": "^2.0.1", - "vue-template-compiler": "^2.6.14" - } - }, - "@nuxt/config": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/config/-/config-2.15.8.tgz", - "integrity": "sha512-KMQbjmUf9RVHeTZEf7zcuFnh03XKZioYhok6GOCY+leu3g5n/UhyPvLnTsgTfsLWohqoRoOm94u4A+tNYwn9VQ==", - "requires": { - "@nuxt/utils": "2.15.8", - "consola": "^2.15.3", - "defu": "^4.0.1", - "destr": "^1.1.0", - "dotenv": "^9.0.2", - "lodash": "^4.17.21", - "rc9": "^1.2.0", - "std-env": "^2.3.0", - "ufo": "^0.7.4" - }, - "dependencies": { - "defu": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-4.0.1.tgz", - "integrity": "sha512-lC+G0KvvWRbisQa50+iFelm3/eMmwo4IlBmfASOVlw9MZpHHyQeVsZxc5j23+TQy5ydgEoTVSrWl7ptou1kzJQ==" - } - } - }, - "@nuxt/core": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/core/-/core-2.15.8.tgz", - "integrity": "sha512-31pipWRvwHiyB5VDqffgSO7JtmHxyzgshIzuZzSinxMbVmK3BKsOwacD/51oEyELgrPlUgLqcY9dg+RURgmHGQ==", - "requires": { - "@nuxt/config": "2.15.8", - "@nuxt/server": "2.15.8", - "@nuxt/utils": "2.15.8", - "consola": "^2.15.3", - "fs-extra": "^9.1.0", - "hable": "^3.0.0", - "hash-sum": "^2.0.0", - "lodash": "^4.17.21" - } - }, - "@nuxt/devalue": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-1.2.5.tgz", - "integrity": "sha512-Tg86C7tqzvZtZli2BQVqgzZN136mZDTgauvJXagglKkP2xt5Kw3NUIiJyjX0Ww/IZy2xVmD0LN+CEPpij4dB2g==", - "requires": { - "consola": "^2.9.0" - } - }, - "@nuxt/friendly-errors-webpack-plugin": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@nuxt/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-2.5.2.tgz", - "integrity": "sha512-LLc+90lnxVbpKkMqk5z1EWpXoODhc6gRkqqXJCInJwF5xabHAE7biFvbULfvTRmtaTzAaP8IV4HQDLUgeAUTTw==", - "requires": { - "chalk": "^2.3.2", - "consola": "^2.6.0", - "error-stack-parser": "^2.0.0", - "string-width": "^4.2.3" - }, - "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==", - "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==", - "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==", - "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==" - }, - "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==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@nuxt/generator": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/generator/-/generator-2.15.8.tgz", - "integrity": "sha512-hreLdYbBIe3SWcP8LsMG7OlDTx2ZVucX8+f8Vrjft3Q4r8iCwLMYC1s1N5etxeHAZfS2kZiLmF92iscOdfbgMQ==", - "requires": { - "@nuxt/utils": "2.15.8", - "chalk": "^4.1.1", - "consola": "^2.15.3", - "defu": "^4.0.1", - "devalue": "^2.0.1", - "fs-extra": "^9.1.0", - "html-minifier": "^4.0.0", - "node-html-parser": "^3.2.0", - "ufo": "^0.7.4" - }, - "dependencies": { - "defu": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-4.0.1.tgz", - "integrity": "sha512-lC+G0KvvWRbisQa50+iFelm3/eMmwo4IlBmfASOVlw9MZpHHyQeVsZxc5j23+TQy5ydgEoTVSrWl7ptou1kzJQ==" - } - } - }, - "@nuxt/loading-screen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nuxt/loading-screen/-/loading-screen-2.0.4.tgz", - "integrity": "sha512-xpEDAoRu75tLUYCkUJCIvJkWJSuwr8pqomvQ+fkXpSrkxZ/9OzlBFjAbVdOAWTMj4aV/LVQso4vcEdircKeFIQ==", - "requires": { - "connect": "^3.7.0", - "defu": "^5.0.0", - "get-port-please": "^2.2.0", - "node-res": "^5.0.1", - "serve-static": "^1.14.1" - }, - "dependencies": { - "defu": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-5.0.1.tgz", - "integrity": "sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==" - } - } - }, - "@nuxt/opencollective": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.3.3.tgz", - "integrity": "sha512-6IKCd+gP0HliixqZT/p8nW3tucD6Sv/u/eR2A9X4rxT/6hXlMzA4GZQzq4d2qnBAwSwGpmKyzkyTjNjrhaA25A==", - "requires": { - "chalk": "^4.1.0", - "consola": "^2.15.0", - "node-fetch": "^2.6.7" - } - }, - "@nuxt/postcss8": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nuxt/postcss8/-/postcss8-1.1.3.tgz", - "integrity": "sha512-CdHtErhvQwueNZPBOmlAAKrNCK7aIpZDYhtS7TzXlSgPHHox1g3cSlf+Ke9oB/8t4mNNjdB+prclme2ibuCOEA==", - "dev": true, - "requires": { - "autoprefixer": "^10.2.5", - "css-loader": "^5.0.0", - "defu": "^3.2.2", - "postcss": "^8.1.10", - "postcss-import": "^13.0.0", - "postcss-loader": "^4.1.0", - "postcss-url": "^10.1.1", - "semver": "^7.3.4" - } - }, - "@nuxt/server": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/server/-/server-2.15.8.tgz", - "integrity": "sha512-E4EtXudxtWQBUHMHOxFwm5DlPOkJbW+iF1+zc0dGmXLscep1KWPrlP+4nrpZj8/UKzpupamE8ZTS9I4IbnExVA==", - "requires": { - "@nuxt/utils": "2.15.8", - "@nuxt/vue-renderer": "2.15.8", - "@nuxtjs/youch": "^4.2.3", - "compression": "^1.7.4", - "connect": "^3.7.0", - "consola": "^2.15.3", - "etag": "^1.8.1", - "fresh": "^0.5.2", - "fs-extra": "^9.1.0", - "ip": "^1.1.5", - "launch-editor-middleware": "^2.2.1", - "on-headers": "^1.0.2", - "pify": "^5.0.0", - "serve-placeholder": "^1.2.3", - "serve-static": "^1.14.1", - "server-destroy": "^1.0.1", - "ufo": "^0.7.4" - } - }, - "@nuxt/telemetry": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-1.3.9.tgz", - "integrity": "sha512-QR+UjZ1VXBsLwNjxe5cFqy5yhU+0x13/+dyjSLfC2vwRRQLPlCCPIAhtB1pvnE6Qxn7hh8psxRYu/efWS7laDA==", - "requires": { - "arg": "^5.0.2", - "chalk": "^4.1.1", - "ci-info": "^3.6.1", - "consola": "^2.15.3", - "create-require": "^1.1.1", - "defu": "^5.0.0", - "destr": "^1.1.0", - "dotenv": "^9.0.2", - "fs-extra": "^8.1.0", - "git-url-parse": "^13.1.0", - "inquirer": "^7.3.3", - "jiti": "^1.9.2", - "nanoid": "^3.1.23", - "node-fetch": "^2.6.1", - "parse-git-config": "^3.0.0", - "rc9": "^1.2.0", - "std-env": "^2.3.0" - }, - "dependencies": { - "defu": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-5.0.1.tgz", - "integrity": "sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==" - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - } - } - }, - "@nuxt/types": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/types/-/types-2.15.8.tgz", - "integrity": "sha512-zBAG5Fy+SIaZIerOVF1vxy1zz16ZK07QSbsuQAjdtEFlvr+vKK+0AqCv8r8DBY5IVqdMIaw5FgNUz5py0xWdPg==", - "requires": { - "@types/autoprefixer": "9.7.2", - "@types/babel__core": "7.1.14", - "@types/compression": "1.7.0", - "@types/connect": "3.4.34", - "@types/etag": "1.8.0", - "@types/file-loader": "5.0.0", - "@types/html-minifier": "4.0.0", - "@types/less": "3.0.2", - "@types/node": "12.20.12", - "@types/optimize-css-assets-webpack-plugin": "5.0.3", - "@types/pug": "2.0.4", - "@types/sass-loader": "8.0.1", - "@types/serve-static": "1.13.9", - "@types/terser-webpack-plugin": "4.2.1", - "@types/webpack": "4.41.28", - "@types/webpack-bundle-analyzer": "3.9.3", - "@types/webpack-dev-middleware": "4.1.2", - "@types/webpack-hot-middleware": "2.25.4", - "sass-loader": "10.1.1" - } - }, - "@nuxt/utils": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/utils/-/utils-2.15.8.tgz", - "integrity": "sha512-e0VBarUbPiQ4ZO1T58puoFIuXme7L5gk1QfwyxOONlp2ryE7aRyZ8X/mryuOiIeyP64c4nwSUtN7q9EUWRb7Lg==", - "requires": { - "consola": "^2.15.3", - "create-require": "^1.1.1", - "fs-extra": "^9.1.0", - "hash-sum": "^2.0.0", - "jiti": "^1.9.2", - "lodash": "^4.17.21", - "proper-lockfile": "^4.1.2", - "semver": "^7.3.5", - "serialize-javascript": "^5.0.1", - "signal-exit": "^3.0.3", - "ua-parser-js": "^0.7.28", - "ufo": "^0.7.4" - } - }, - "@nuxt/vue-app": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/vue-app/-/vue-app-2.15.8.tgz", - "integrity": "sha512-FJf9FSMPsWT3BqkS37zEuPTxLKzSg2EIwp1sP8Eou25eE08qxRfe2PwTVA8HnXUPNdpz2uk/T9DlNw+JraiFRQ==", - "requires": { - "node-fetch": "^2.6.1", - "ufo": "^0.7.4", - "unfetch": "^4.2.0", - "vue": "^2.6.12", - "vue-client-only": "^2.0.0", - "vue-meta": "^2.4.0", - "vue-no-ssr": "^1.1.1", - "vue-router": "^3.5.1", - "vue-template-compiler": "^2.6.12", - "vuex": "^3.6.2" - } - }, - "@nuxt/vue-renderer": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/vue-renderer/-/vue-renderer-2.15.8.tgz", - "integrity": "sha512-54I/k+4G6axP9XVYYdtH6M1S6T49OIkarpF6/yIJj0yi3S/2tdJ9eUyfoLZ9EbquZFDDRHBxSswTtr2l/eakPw==", - "requires": { - "@nuxt/devalue": "^1.2.5", - "@nuxt/utils": "2.15.8", - "consola": "^2.15.3", - "defu": "^4.0.1", - "fs-extra": "^9.1.0", - "lodash": "^4.17.21", - "lru-cache": "^5.1.1", - "ufo": "^0.7.4", - "vue": "^2.6.12", - "vue-meta": "^2.4.0", - "vue-server-renderer": "^2.6.12" - }, - "dependencies": { - "defu": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-4.0.1.tgz", - "integrity": "sha512-lC+G0KvvWRbisQa50+iFelm3/eMmwo4IlBmfASOVlw9MZpHHyQeVsZxc5j23+TQy5ydgEoTVSrWl7ptou1kzJQ==" - } - } - }, - "@nuxt/webpack": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/@nuxt/webpack/-/webpack-2.15.8.tgz", - "integrity": "sha512-CzJYFed23Ow/UK0+cI1FVthDre1p2qc8Q97oizG39d3/SIh3aUHjgj8c60wcR+RSxVO0FzZMXkmq02NmA7vWJg==", - "requires": { - "@babel/core": "^7.14.0", - "@nuxt/babel-preset-app": "2.15.8", - "@nuxt/friendly-errors-webpack-plugin": "^2.5.1", - "@nuxt/utils": "2.15.8", - "babel-loader": "^8.2.2", - "cache-loader": "^4.1.0", - "caniuse-lite": "^1.0.30001228", - "consola": "^2.15.3", - "css-loader": "^4.3.0", - "cssnano": "^4.1.11", - "eventsource-polyfill": "^0.9.6", - "extract-css-chunks-webpack-plugin": "^4.9.0", - "file-loader": "^6.2.0", - "glob": "^7.1.7", - "hard-source-webpack-plugin": "^0.13.1", - "hash-sum": "^2.0.0", - "html-webpack-plugin": "^4.5.1", - "lodash": "^4.17.21", - "memory-fs": "^0.5.0", - "optimize-css-assets-webpack-plugin": "^5.0.4", - "pify": "^5.0.0", - "pnp-webpack-plugin": "^1.6.4", - "postcss": "^7.0.32", - "postcss-import": "^12.0.1", - "postcss-import-resolver": "^2.0.0", - "postcss-loader": "^3.0.0", - "postcss-preset-env": "^6.7.0", - "postcss-url": "^8.0.0", - "semver": "^7.3.5", - "std-env": "^2.3.0", - "style-resources-loader": "^1.4.1", - "terser-webpack-plugin": "^4.2.3", - "thread-loader": "^3.0.4", - "time-fix-plugin": "^2.0.7", - "ufo": "^0.7.4", - "url-loader": "^4.1.1", - "vue-loader": "^15.9.7", - "vue-style-loader": "^4.1.3", - "vue-template-compiler": "^2.6.12", - "webpack": "^4.46.0", - "webpack-bundle-analyzer": "^4.4.1", - "webpack-dev-middleware": "^4.2.0", - "webpack-hot-middleware": "^2.25.0", - "webpack-node-externals": "^3.0.0", - "webpackbar": "^4.0.0" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" - }, - "css-loader": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-4.3.0.tgz", - "integrity": "sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg==", - "requires": { - "camelcase": "^6.0.0", - "cssesc": "^3.0.0", - "icss-utils": "^4.1.1", - "loader-utils": "^2.0.0", - "postcss": "^7.0.32", - "postcss-modules-extract-imports": "^2.0.0", - "postcss-modules-local-by-default": "^3.0.3", - "postcss-modules-scope": "^2.2.0", - "postcss-modules-values": "^3.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^2.7.1", - "semver": "^7.3.2" - } - }, - "icss-utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", - "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", - "requires": { - "postcss": "^7.0.14" - } - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-import": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-12.0.1.tgz", - "integrity": "sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==", - "requires": { - "postcss": "^7.0.1", - "postcss-value-parser": "^3.2.3", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-load-config": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.2.tgz", - "integrity": "sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==", - "requires": { - "cosmiconfig": "^5.0.0", - "import-cwd": "^2.0.0" - } - }, - "postcss-loader": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", - "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", - "requires": { - "loader-utils": "^1.1.0", - "postcss": "^7.0.0", - "postcss-load-config": "^2.0.0", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "postcss-modules-extract-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", - "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", - "requires": { - "postcss": "^7.0.5" - } - }, - "postcss-modules-local-by-default": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", - "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", - "requires": { - "icss-utils": "^4.1.1", - "postcss": "^7.0.32", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - } - }, - "postcss-modules-scope": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", - "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", - "requires": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0" - } - }, - "postcss-modules-values": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", - "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", - "requires": { - "icss-utils": "^4.0.0", - "postcss": "^7.0.6" - } - }, - "postcss-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-8.0.0.tgz", - "integrity": "sha512-E2cbOQ5aii2zNHh8F6fk1cxls7QVFZjLPSrqvmiza8OuXLzIpErij8BDS5Y3STPfJgpIMNCPEr8JlKQWEoozUw==", - "requires": { - "mime": "^2.3.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.0", - "postcss": "^7.0.2", - "xxhashjs": "^0.2.1" - } - }, - "schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "requires": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - } - } - } - }, - "@nuxtjs/axios": { - "version": "5.13.6", - "resolved": "https://registry.npmjs.org/@nuxtjs/axios/-/axios-5.13.6.tgz", - "integrity": "sha512-XS+pOE0xsDODs1zAIbo95A0LKlilvJi8YW0NoXYuq3/jjxGgWDxizZ6Yx0AIIjZOoGsXJOPc0/BcnSEUQ2mFBA==", - "requires": { - "@nuxtjs/proxy": "^2.1.0", - "axios": "^0.21.1", - "axios-retry": "^3.1.9", - "consola": "^2.15.3", - "defu": "^5.0.0" - }, - "dependencies": { - "defu": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-5.0.1.tgz", - "integrity": "sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==" - } - } - }, - "@nuxtjs/proxy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nuxtjs/proxy/-/proxy-2.1.0.tgz", - "integrity": "sha512-/qtoeqXgZ4Mg6LRg/gDUZQrFpOlOdHrol/vQYMnKu3aN3bP90UfOUB3QSDghUUK7OISAJ0xp8Ld78aHyCTcKCQ==", - "requires": { - "http-proxy-middleware": "^1.0.6" - } - }, - "@nuxtjs/pwa": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@nuxtjs/pwa/-/pwa-3.3.5.tgz", - "integrity": "sha512-8tTmW8DBspWxlJwTimOHTkwfkwPpL9wIcGmy75Gcmin+c9YtX2Ehxmhgt/TLFOC9XsLAqojqynw3/Agr/9OE1w==", - "dev": true, - "requires": { - "clone-deep": "^4.0.1", - "defu": "^3.2.2", - "execa": "^5.0.0", - "fs-extra": "^9.1.0", - "hasha": "^5.2.2", - "jimp-compact": "^0.16.1", - "lodash.template": "^4.5.0", - "serve-static": "^1.14.1", - "workbox-cdn": "^5.1.4" - } - }, - "@nuxtjs/tailwindcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@nuxtjs/tailwindcss/-/tailwindcss-4.2.1.tgz", - "integrity": "sha512-Sku7VETunn5YmvzkXFDuRdP1gUGau02eh5HtcAsI9//1hpSuEN49n3XhatBrPOXQ57WhUlnjXf0/LjT9KQH0+A==", - "dev": true, - "requires": { - "@nuxt/postcss8": "^1.1.3", - "autoprefixer": "^10.2.5", - "chalk": "^4.1.1", - "clear-module": "^4.1.1", - "consola": "^2.15.3", - "defu": "^5.0.0", - "postcss": "^8.3.5", - "postcss-custom-properties": "^11.0.0", - "postcss-nesting": "^8.0.1", - "tailwind-config-viewer": "^1.6.2", - "tailwindcss": "^2.2.2", - "ufo": "^0.7.5" - }, - "dependencies": { - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dev": true, - "requires": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - } - }, - "cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "defu": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-5.0.1.tgz", - "integrity": "sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==", - "dev": true - }, - "fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "html-tags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", - "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", - "dev": true - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "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" - } - }, - "postcss-js": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", - "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", - "dev": true, - "requires": { - "camelcase-css": "^2.0.1", - "postcss": "^8.1.6" - } - }, - "postcss-nested": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", - "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.6" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "tailwindcss": { - "version": "2.2.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.19.tgz", - "integrity": "sha512-6Ui7JSVtXadtTUo2NtkBBacobzWiQYVjYW0ZnKaP9S1ZCKQ0w7KVNz+YSDI/j7O7KCMHbOkz94ZMQhbT9pOqjw==", - "dev": true, - "requires": { - "arg": "^5.0.1", - "bytes": "^3.0.0", - "chalk": "^4.1.2", - "chokidar": "^3.5.2", - "color": "^4.0.1", - "cosmiconfig": "^7.0.1", - "detective": "^5.2.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.7", - "fs-extra": "^10.0.0", - "glob-parent": "^6.0.1", - "html-tags": "^3.1.0", - "is-color-stop": "^1.1.0", - "is-glob": "^4.0.1", - "lodash": "^4.17.21", - "lodash.topath": "^4.5.2", - "modern-normalize": "^1.1.0", - "node-emoji": "^1.11.0", - "normalize-path": "^3.0.0", - "object-hash": "^2.2.0", - "postcss-js": "^3.0.3", - "postcss-load-config": "^3.1.0", - "postcss-nested": "5.0.6", - "postcss-selector-parser": "^6.0.6", - "postcss-value-parser": "^4.1.0", - "pretty-hrtime": "^1.0.3", - "purgecss": "^4.0.3", - "quick-lru": "^5.1.1", - "reduce-css-calc": "^2.1.8", - "resolve": "^1.20.0", - "tmp": "^0.2.1" - } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } - } - } - }, - "@nuxtjs/youch": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@nuxtjs/youch/-/youch-4.2.3.tgz", - "integrity": "sha512-XiTWdadTwtmL/IGkNqbVe+dOlT+IMvcBu7TvKI7plWhVQeBCQ9iKhk3jgvVWFyiwL2yHJDlEwOM5v9oVES5Xmw==", - "requires": { - "cookie": "^0.3.1", - "mustache": "^2.3.0", - "stack-trace": "0.0.10" - } - }, - "@polka/url": { - "version": "1.0.0-next.21", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", - "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==" - }, - "@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==" - }, - "@teckel/vue-pdf": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@teckel/vue-pdf/-/vue-pdf-4.3.5.tgz", - "integrity": "sha512-g2DAbZMPbPc7NPFImOsU/e7rt7wfdmBkmFa2kPsB4x+k+Bs8yC5Icmq/VnTSEq/Y8bNvEY7i6+JoicGnlfQL7Q==", - "requires": { - "babel-plugin-syntax-dynamic-import": "^6.18.0", - "loader-utils": "^1.4.0", - "pdfjs-dist": "^2.5.207 <2.8.0", - "raw-loader": "^4.0.1", - "vue-resize-sensor": "^2.0.0", - "worker-loader": "^2.0.0" - }, - "dependencies": { - "json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - } - } - }, - "@types/anymatch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz", - "integrity": "sha512-qLChUo6yhpQ9k905NwL74GU7TxH+9UODwwQ6ICNI+O6EDMExqH/Cv9NsbmcZ7yC/rRXJ/AHCzfgjsFRY5fKjYw==", - "requires": { - "anymatch": "*" - } - }, - "@types/autoprefixer": { - "version": "9.7.2", - "resolved": "https://registry.npmjs.org/@types/autoprefixer/-/autoprefixer-9.7.2.tgz", - "integrity": "sha512-QX7U7YW3zX3ex6MECtWO9folTGsXeP4b8bSjTq3I1ODM+H+sFHwGKuof+T+qBcDClGlCGtDb3SVfiTVfmcxw4g==", - "requires": { - "@types/browserslist": "*", - "postcss": "7.x.x" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "@types/babel__core": { - "version": "7.1.14", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz", - "integrity": "sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==", - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", - "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/browserslist": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/@types/browserslist/-/browserslist-4.15.0.tgz", - "integrity": "sha512-h9LyKErRGZqMsHh9bd+FE8yCIal4S0DxKTOeui56VgVXqa66TKiuaIUxCAI7c1O0LjaUzOTcsMyOpO9GetozRA==", - "requires": { - "browserslist": "*" - } - }, - "@types/clean-css": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.6.tgz", - "integrity": "sha512-Ze1tf+LnGPmG6hBFMi0B4TEB0mhF7EiMM5oyjLDNPE9hxrPU0W+5+bHvO+eFPA+bt0iC1zkQMoU/iGdRVjcRbw==", - "requires": { - "@types/node": "*", - "source-map": "^0.6.0" - } - }, - "@types/compression": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.0.tgz", - "integrity": "sha512-3LzWUM+3k3XdWOUk/RO+uSjv7YWOatYq2QADJntK1pjkk4DfVP0KrIEPDnXRJxAAGKe0VpIPRmlINLDuCedZWw==", - "requires": { - "@types/express": "*" - } - }, - "@types/connect": { - "version": "3.4.34", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", - "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", - "requires": { - "@types/node": "*" - } - }, - "@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.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" - }, - "@types/etag": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@types/etag/-/etag-1.8.0.tgz", - "integrity": "sha512-EdSN0x+Y0/lBv7YAb8IU4Jgm6DWM+Bqtz7o5qozl96fzaqdqbdfHS5qjdpFeIv7xQ8jSLyjMMNShgYtMajEHyQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/file-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/file-loader/-/file-loader-5.0.0.tgz", - "integrity": "sha512-evodFzM0PLOXmMZy8DhPN+toP6QgJiIteF6e8iD9T0xGBUllQA/DAb1nZwCIoNh7vuLvqCGPUdsLf3GSbcHd4g==", - "requires": { - "@types/webpack": "^4" - } - }, - "@types/html-minifier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier/-/html-minifier-4.0.0.tgz", - "integrity": "sha512-eFnGhrKmjWBlnSGNtunetE3UU2Tc/LUl92htFslSSTmpp9EKHQVcYQadCyYfnzUEFB5G/3wLWo/USQS/mEPKrA==", - "requires": { - "@types/clean-css": "*", - "@types/relateurl": "*", - "@types/uglify-js": "*" - } - }, - "@types/html-minifier-terser": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", - "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==" - }, - "@types/http-proxy": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", - "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", - "requires": { - "@types/node": "*" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" - }, - "@types/less": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/less/-/less-3.0.2.tgz", - "integrity": "sha512-62vfe65cMSzYaWmpmhqCMMNl0khen89w57mByPi1OseGfcV/LV03fO8YVrNj7rFQsRWNJo650WWyh6m7p8vZmA==" - }, - "@types/localforage": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz", - "integrity": "sha512-tJxahnjm9dEI1X+hQSC5f2BSd/coZaqbIl1m3TCl0q9SVuC52XcXfV0XmoCU1+PmjyucuVITwoTnN8OlTbEXXA==", - "requires": { - "localforage": "*" - } - }, - "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" - }, - "@types/node": { - "version": "12.20.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.12.tgz", - "integrity": "sha512-KQZ1al2hKOONAs2MFv+yTQP1LkDWMrRJ9YCVRalXltOfXsBmH5IownLxQaiq0lnAHwAViLnh2aTYqrPcRGEbgg==" - }, - "@types/node-sass": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@types/node-sass/-/node-sass-4.11.3.tgz", - "integrity": "sha512-wXPCn3t9uu5rR4zXNSLasZHQMuRzUKBsdi4MsgT8uq4Lp1gQQo+T2G23tGj4SSgDHeNBle6vGseZtM2XV/X9bw==", - "requires": { - "@types/node": "*" - } - }, - "@types/optimize-css-assets-webpack-plugin": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz", - "integrity": "sha512-PJgbI4KplJfyxKWVrBbEL+rePEBqeozJRMT0mBL3ynhvngASBV/XJ+BneLuJN74RjjMzO0gA5ns80mgubQdZAA==", - "requires": { - "@types/webpack": "^4" - } - }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, - "@types/pug": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.4.tgz", - "integrity": "sha512-cevKhB0yUJCFKzCnkB6HbDRZdYwVRRXzhIKRDgfAR1dnzEwZLRGf5lKpLJLZEP/odmaWT+gWNwH02bRhQIBYPg==" - }, - "@types/q": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", - "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "@types/relateurl": { - "version": "0.2.29", - "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.29.tgz", - "integrity": "sha512-QSvevZ+IRww2ldtfv1QskYsqVVVwCKQf1XbwtcyyoRvLIQzfyPhj/C+3+PKzSDRdiyejaiLgnq//XTkleorpLg==" - }, - "@types/sass": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.43.1.tgz", - "integrity": "sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==", - "requires": { - "@types/node": "*" - } - }, - "@types/sass-loader": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@types/sass-loader/-/sass-loader-8.0.1.tgz", - "integrity": "sha512-kum0/5Im5K2WdDTRsLtrXXvX2VJc3rgq9favK+vIdWLn35miWUIYuPkiQlLCHks9//sZ3GWYs4uYzCdmoKKLcQ==", - "requires": { - "@types/node-sass": "*", - "@types/sass": "*", - "@types/webpack": "^4" - } - }, - "@types/serve-static": { - "version": "1.13.9", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", - "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/source-list-map": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==" - }, - "@types/tapable": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", - "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==" - }, - "@types/terser-webpack-plugin": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-4.2.1.tgz", - "integrity": "sha512-x688KsgQKJF8PPfv4qSvHQztdZNHLlWJdolN9/ptAGimHVy3rY+vHdfglQDFh1Z39h7eMWOd6fQ7ke3PKQcdyA==", - "requires": { - "@types/webpack": "^4", - "terser": "^4.6.13" - } - }, - "@types/uglify-js": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz", - "integrity": "sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g==", - "requires": { - "source-map": "^0.6.1" - } - }, - "@types/webpack": { - "version": "4.41.28", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.28.tgz", - "integrity": "sha512-Nn84RAiJjKRfPFFCVR8LC4ueTtTdfWAMZ03THIzZWRJB+rX24BD3LqPSFnbMscWauEsT4segAsylPDIaZyZyLQ==", - "requires": { - "@types/anymatch": "*", - "@types/node": "*", - "@types/tapable": "^1", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "source-map": "^0.6.0" - } - }, - "@types/webpack-bundle-analyzer": { - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.3.tgz", - "integrity": "sha512-l/vaDMWGcXiMB3CbczpyICivLTB07/JNtn1xebsRXE9tPaUDEHgX3x7YP6jfznG5TOu7I4w0Qx1tZz61znmPmg==", - "requires": { - "@types/webpack": "^4" - } - }, - "@types/webpack-dev-middleware": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@types/webpack-dev-middleware/-/webpack-dev-middleware-4.1.2.tgz", - "integrity": "sha512-SxXzPCqeZ03fJ2dg3iD7cSXvqZymmS5/2GD9fANRcyWN7HYK1H3ty6q7IInXZKvPrdUqij831G3RLIeKK6aGdw==", - "requires": { - "@types/connect": "*", - "@types/webpack": "^4" - } - }, - "@types/webpack-hot-middleware": { - "version": "2.25.4", - "resolved": "https://registry.npmjs.org/@types/webpack-hot-middleware/-/webpack-hot-middleware-2.25.4.tgz", - "integrity": "sha512-6tQb9EBKIANZYUVLQYWiWfDFVe7FhXSj4bB2EF5QB7VtYWL3HDR+y/zqjZPAnCorv0spLqVMRqjRK8AmhfocMw==", - "requires": { - "@types/connect": "*", - "@types/webpack": "^4" - } - }, - "@types/webpack-sources": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", - "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", - "requires": { - "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" - } - } - }, - "@vue/babel-helper-vue-jsx-merge-props": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz", - "integrity": "sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==" - }, - "@vue/babel-plugin-transform-vue-jsx": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.4.0.tgz", - "integrity": "sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA==", - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "html-tags": "^2.0.0", - "lodash.kebabcase": "^4.1.1", - "svg-tags": "^1.0.0" - } - }, - "@vue/babel-preset-jsx": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-preset-jsx/-/babel-preset-jsx-1.4.0.tgz", - "integrity": "sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==", - "requires": { - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "@vue/babel-sugar-composition-api-inject-h": "^1.4.0", - "@vue/babel-sugar-composition-api-render-instance": "^1.4.0", - "@vue/babel-sugar-functional-vue": "^1.4.0", - "@vue/babel-sugar-inject-h": "^1.4.0", - "@vue/babel-sugar-v-model": "^1.4.0", - "@vue/babel-sugar-v-on": "^1.4.0" - } - }, - "@vue/babel-sugar-composition-api-inject-h": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.4.0.tgz", - "integrity": "sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g==", - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@vue/babel-sugar-composition-api-render-instance": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.4.0.tgz", - "integrity": "sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q==", - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@vue/babel-sugar-functional-vue": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.4.0.tgz", - "integrity": "sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw==", - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@vue/babel-sugar-inject-h": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.4.0.tgz", - "integrity": "sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA==", - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@vue/babel-sugar-v-model": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.4.0.tgz", - "integrity": "sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ==", - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "camelcase": "^5.0.0", - "html-tags": "^2.0.0", - "svg-tags": "^1.0.0" - } - }, - "@vue/babel-sugar-v-on": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.4.0.tgz", - "integrity": "sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA==", - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "camelcase": "^5.0.0" - } - }, - "@vue/compiler-sfc": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz", - "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==", - "requires": { - "@babel/parser": "^7.18.4", - "postcss": "^8.4.14", - "source-map": "^0.6.1" - } - }, - "@vue/component-compiler-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", - "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", - "requires": { - "consolidate": "^0.15.1", - "hash-sum": "^1.0.2", - "lru-cache": "^4.1.2", - "merge-source-map": "^1.1.0", - "postcss": "^7.0.36", - "postcss-selector-parser": "^6.0.2", - "prettier": "^1.18.2 || ^2.0.0", - "source-map": "~0.6.1", - "vue-template-es2015-compiler": "^1.9.0" - }, - "dependencies": { - "hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==" - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" - } - } - }, - "@webassemblyjs/ast": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", - "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", - "requires": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", - "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==" - }, - "@webassemblyjs/helper-api-error": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", - "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==" - }, - "@webassemblyjs/helper-buffer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", - "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==" - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", - "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", - "requires": { - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "@webassemblyjs/helper-fsm": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", - "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==" - }, - "@webassemblyjs/helper-module-context": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", - "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", - "requires": { - "@webassemblyjs/ast": "1.9.0" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==" - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", - "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", - "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", - "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", - "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==" - }, - "@webassemblyjs/wasm-edit": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", - "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/helper-wasm-section": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-opt": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", - "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", - "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", - "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "@webassemblyjs/wast-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", - "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/floating-point-hex-parser": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-code-frame": "1.9.0", - "@webassemblyjs/helper-fsm": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", - "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "@xmldom/xmldom": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.9.tgz", - "integrity": "sha512-yceMpm/xd4W2a85iqZyO09gTnHvXF6pyiWjD2jcOJs7hRoZtNNOO1eJlhHj1ixA+xip2hOyGn+LgcvLCMo5zXA==" - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" - }, - "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" - } - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - }, - "acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "requires": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "requires": {} - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "requires": {} - }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==" - }, - "ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "requires": { - "string-width": "^4.1.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==", - "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "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==" - } - } - }, - "ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==" - }, - "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==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==" - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==" - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==" - }, - "array.prototype.reduce": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", - "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" - } - }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==" - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==", - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==" - }, - "async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "optional": true - }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" - }, - "autoprefixer": { - "version": "10.4.13", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", - "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", - "dev": true, - "requires": { - "browserslist": "^4.21.4", - "caniuse-lite": "^1.0.30001426", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - } - }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - }, - "axios-retry": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.3.1.tgz", - "integrity": "sha512-RohAUQTDxBSWLFEnoIG/6bvmy8l3TfpkclgStjl5MDCMBDgapAWCmr1r/9harQfWC8bzLC8job6UcL1A1Yc+/Q==", - "requires": { - "@babel/runtime": "^7.15.4", - "is-retry-allowed": "^2.2.0" - } - }, - "babel-loader": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", - "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", - "requires": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "dependencies": { - "schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "requires": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - } - } - } - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "requires": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.3" - } - }, - "babel-plugin-syntax-dynamic-import": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", - "integrity": "sha512-MioUE+LfjCEz65Wf7Z/Rm4XCP5k2c+TbMd2Z2JKc7U9uwjBhAfNPE48KC4GTGKhppMeYVepwDBNO/nGY6NYHBA==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, - "boxen": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", - "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", - "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^6.2.0", - "chalk": "^4.1.0", - "cli-boxes": "^2.2.1", - "string-width": "^4.2.2", - "type-fest": "^0.20.2", - "widest-line": "^3.1.0", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" - } - } - }, - "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==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "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==" - } - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "requires": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" - } - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "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==" - }, - "buffer-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/buffer-json/-/buffer-json-2.0.0.tgz", - "integrity": "sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==" - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==" - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" - }, - "cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "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" - }, - "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" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "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" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "cache-content-type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", - "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", - "dev": true, - "requires": { - "mime-types": "^2.1.18", - "ylru": "^1.2.0" - } - }, - "cache-loader": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-4.1.0.tgz", - "integrity": "sha512-ftOayxve0PwKzBF/GLsZNC9fJBXl8lkZE3TOsjkboHfVHVkL39iUEs1FO07A33mizmci5Dudt38UZrrYXDtbhw==", - "requires": { - "buffer-json": "^2.0.0", - "find-cache-dir": "^3.0.0", - "loader-utils": "^1.2.3", - "mkdirp": "^0.5.1", - "neo-async": "^2.6.1", - "schema-utils": "^2.0.0" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "requires": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - } - } - } - }, - "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" - } - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", - "requires": { - "callsites": "^2.0.0" - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==" - }, - "camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", - "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true - }, - "caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "requires": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001431", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", - "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==" - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "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==" - }, - "chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" - }, - "ci-info": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz", - "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==" - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - } - }, - "clean-css": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", - "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", - "requires": { - "source-map": "~0.6.0" - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" - }, - "clear-module": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/clear-module/-/clear-module-4.1.2.tgz", - "integrity": "sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw==", - "dev": true, - "requires": { - "parent-module": "^2.0.0", - "resolve-from": "^5.0.0" - } - }, - "cli-boxes": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==" - }, - "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" - } - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.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 - }, - "coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "requires": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.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==", - "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==", - "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==", - "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==" - }, - "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==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "requires": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - }, - "dependencies": { - "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==", - "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==" - } - } - }, - "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==", - "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==" - }, - "color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "dependencies": { - "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" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "requires": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, - "dependencies": { - "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" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" - }, - "console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" - }, - "consolidate": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", - "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", - "requires": { - "bluebird": "^3.1.1" - } - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==" - }, - "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==", - "dev": true, - "requires": { - "safe-buffer": "5.2.1" - }, - "dependencies": { - "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==", - "dev": true - } - } - }, - "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==", - "dev": true - }, - "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==" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==" - }, - "cookies": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", - "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", - "dev": true, - "requires": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - } - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==" - }, - "core-js": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz", - "integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==" - }, - "core-js-compat": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz", - "integrity": "sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==", - "requires": { - "browserslist": "^4.21.4" - } - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "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" - } - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - } - }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "requires": { - "buffer": "^5.1.0" - } - }, - "create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - }, - "cron-parser": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.1.tgz", - "integrity": "sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==", - "requires": { - "luxon": "^3.2.1" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "css-blank-pseudo": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", - "integrity": "sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==", - "requires": { - "postcss": "^7.0.5" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==" - }, - "css-declaration-sorter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", - "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", - "requires": { - "postcss": "^7.0.1", - "timsort": "^0.3.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "css-has-pseudo": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz", - "integrity": "sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==", - "requires": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^5.0.0-rc.4" - }, - "dependencies": { - "cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==" - }, - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "requires": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "css-loader": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", - "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", - "requires": { - "icss-utils": "^5.1.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.15", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.5" - } - }, - "css-prefers-color-scheme": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz", - "integrity": "sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==", - "requires": { - "postcss": "^7.0.5" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "requires": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - } - }, - "css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" - }, - "css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", - "requires": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - } - }, - "css-unit-converter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", - "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==", - "dev": true - }, - "css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" - }, - "cssdb": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz", - "integrity": "sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==" - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" - }, - "cssnano": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz", - "integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==", - "requires": { - "cosmiconfig": "^5.0.0", - "cssnano-preset-default": "^4.0.8", - "is-resolvable": "^1.0.0", - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "cssnano-preset-default": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz", - "integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==", - "requires": { - "css-declaration-sorter": "^4.0.1", - "cssnano-util-raw-cache": "^4.0.1", - "postcss": "^7.0.0", - "postcss-calc": "^7.0.1", - "postcss-colormin": "^4.0.3", - "postcss-convert-values": "^4.0.1", - "postcss-discard-comments": "^4.0.2", - "postcss-discard-duplicates": "^4.0.2", - "postcss-discard-empty": "^4.0.1", - "postcss-discard-overridden": "^4.0.1", - "postcss-merge-longhand": "^4.0.11", - "postcss-merge-rules": "^4.0.3", - "postcss-minify-font-values": "^4.0.2", - "postcss-minify-gradients": "^4.0.2", - "postcss-minify-params": "^4.0.2", - "postcss-minify-selectors": "^4.0.2", - "postcss-normalize-charset": "^4.0.1", - "postcss-normalize-display-values": "^4.0.2", - "postcss-normalize-positions": "^4.0.2", - "postcss-normalize-repeat-style": "^4.0.2", - "postcss-normalize-string": "^4.0.2", - "postcss-normalize-timing-functions": "^4.0.2", - "postcss-normalize-unicode": "^4.0.1", - "postcss-normalize-url": "^4.0.1", - "postcss-normalize-whitespace": "^4.0.2", - "postcss-ordered-values": "^4.1.2", - "postcss-reduce-initial": "^4.0.3", - "postcss-reduce-transforms": "^4.0.2", - "postcss-svgo": "^4.0.3", - "postcss-unique-selectors": "^4.0.1" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "cssnano-util-get-arguments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", - "integrity": "sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw==" - }, - "cssnano-util-get-match": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", - "integrity": "sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw==" - }, - "cssnano-util-raw-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", - "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "cssnano-util-same-parent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", - "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==" - }, - "csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "requires": { - "css-tree": "^1.1.2" - }, - "dependencies": { - "css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "requires": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - } - }, - "mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" - } - } - }, - "csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, - "cuint": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", - "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==" - }, - "cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" - }, - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" - }, - "de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==" - }, - "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" - } - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==" - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", - "dev": true - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" - }, - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "dev": true - }, - "defu": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/defu/-/defu-3.2.2.tgz", - "integrity": "sha512-8UWj5lNv7HD+kB0e9w77Z7TdQlbUYDVWqITLHNqFIn6khrNHv5WQo38Dcm1f6HeNyZf0U7UbPf6WeZDSdCzGDQ==", - "dev": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "destr": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/destr/-/destr-1.2.1.tgz", - "integrity": "sha512-ud8w0qMLlci6iFG7CNgeRr8OcbUWMsbfjtWft1eJ5Luqrz/M8Ebqk/KCzne8rKUlIQWWfLv0wD6QHrqOf4GshA==" - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==" - }, - "detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dev": true, - "requires": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - } - }, - "devalue": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-2.0.1.tgz", - "integrity": "sha512-I2TiqT5iWBEyB8GRfTDP0hiLZ0YeDJZ+upDxjBfOC2lebO5LezQMv7QvIUTzdb64jQyAKLf1AHADtGN+jw6v8Q==" - }, - "didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "requires": { - "path-type": "^4.0.0" - } - }, - "dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "requires": { - "utila": "~0.4" - } - }, - "dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" - }, - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" - }, - "domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "requires": { - "domelementtype": "^2.2.0" - } - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - }, - "dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - }, - "dependencies": { - "lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "requires": { - "tslib": "^2.0.3" - } - }, - "no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "requires": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - } - } - }, - "dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "requires": { - "is-obj": "^2.0.0" - } - }, - "dotenv": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", - "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==" - }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "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.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" - }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "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==" - }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "engine.io": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", - "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", - "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==" - } - } - }, - "engine.io-client": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.3.tgz", - "integrity": "sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.2.3", - "xmlhttprequest-ssl": "~2.0.0" - } - }, - "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==" - }, - "enhanced-resolve": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", - "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - } - }, - "entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" - }, - "epubjs": { - "version": "0.3.93", - "resolved": "https://registry.npmjs.org/epubjs/-/epubjs-0.3.93.tgz", - "integrity": "sha512-c06pNSdBxcXv3dZSbXAVLE1/pmleRhOT6mXNZo6INKmvuKpYB65MwU/lO7830czCtjIiK9i+KR+3S+p0wtljrw==", - "requires": { - "@types/localforage": "0.0.34", - "@xmldom/xmldom": "^0.7.5", - "core-js": "^3.18.3", - "event-emitter": "^0.3.5", - "jszip": "^3.7.1", - "localforage": "^1.10.0", - "lodash": "^4.17.21", - "marks-pane": "^1.0.9", - "path-webpack": "0.0.3" - } - }, - "errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "requires": { - "prr": "~1.0.1" - } - }, - "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==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "requires": { - "stackframe": "^1.3.4" - } - }, - "es-abstract": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", - "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", - "requires": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "next-tick": "^1.1.0" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "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==" - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "eventsource-polyfill": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/eventsource-polyfill/-/eventsource-polyfill-0.9.6.tgz", - "integrity": "sha512-LyMFp2oPDGhum2lMvkjqKZEwWd2/AoXyt8aoyftTBMWwPHNgU+2tdxhTHPluDxoz+z4gNj0uHAPR9nqevATMbg==" - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "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==" - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "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" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "requires": { - "type": "^2.7.2" - }, - "dependencies": { - "type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" - } - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extract-css-chunks-webpack-plugin": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/extract-css-chunks-webpack-plugin/-/extract-css-chunks-webpack-plugin-4.9.0.tgz", - "integrity": "sha512-HNuNPCXRMqJDQ1OHAUehoY+0JVCnw9Y/H22FQzYVwo8Ulgew98AGDu0grnY5c7xwiXHjQa6yJ/1dxLCI/xqTyQ==", - "requires": { - "loader-utils": "^2.0.0", - "normalize-url": "1.9.1", - "schema-utils": "^1.0.0", - "webpack-sources": "^1.1.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "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==" - }, - "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "requires": { - "reusify": "^1.0.4" - } - }, - "figgy-pudding": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", - "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==" - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - } - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "optional": true - }, - "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==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "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" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "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==", - "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==", - "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==" - }, - "flatten": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", - "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==" - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==" - }, - "fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", - "dev": true - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "fs-memo": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fs-memo/-/fs-memo-1.2.0.tgz", - "integrity": "sha512-YEexkCpL4j03jn5SxaMHqcO6IuWuqm8JFUYhyCep7Ao89JIYmB8xoKhK7zXXJ9cCaNXpyNH5L3QtAmoxjoHW2w==" - }, - "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-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==" - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==", - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "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==", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - } - }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" - }, - "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==" - }, - "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", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "get-port-please": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-2.6.1.tgz", - "integrity": "sha512-4PDSrL6+cuMM1xs6w36ZIkaKzzE0xzfVBCfebHIJ3FE8iB9oic/ECwPw3iNiD4h1AoJ5XLLBhEviFAVrZsDC5A==", - "requires": { - "fs-memo": "^1.2.0" - } - }, - "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==" - }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==" - }, - "git-config-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-config-path/-/git-config-path-2.0.0.tgz", - "integrity": "sha512-qc8h1KIQbJpp+241id3GuAtkdyJ+IK+LIVtkiFTRKRrmddDzs3SI9CvP1QYmWBFvm1I/PWRwj//of8bgAc0ltA==" - }, - "git-up": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", - "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", - "requires": { - "is-ssh": "^1.4.0", - "parse-url": "^8.1.0" - } - }, - "git-url-parse": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz", - "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==", - "requires": { - "git-up": "^7.0.0" - } - }, - "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==", - "requires": { - "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==" - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "requires": { - "duplexer": "^0.1.2" - } - }, - "hable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hable/-/hable-3.0.0.tgz", - "integrity": "sha512-7+G0/2/COR8pwteYFqHIVYfQpuEiO2HXwJrhCBJVgrNrl9O5eaUoJVDGXUJX+0RpGncNVTuestexjk1afj01wQ==" - }, - "hard-source-webpack-plugin": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.13.1.tgz", - "integrity": "sha512-r9zf5Wq7IqJHdVAQsZ4OP+dcUSvoHqDMxJlIzaE2J0TZWn3UjMMrHqwDHR8Jr/pzPfG7XxSe36E7Y8QGNdtuAw==", - "requires": { - "chalk": "^2.4.1", - "find-cache-dir": "^2.0.0", - "graceful-fs": "^4.1.11", - "lodash": "^4.15.0", - "mkdirp": "^0.5.1", - "node-object-hash": "^1.2.0", - "parse-json": "^4.0.0", - "pkg-dir": "^3.0.0", - "rimraf": "^2.6.2", - "semver": "^5.6.0", - "tapable": "^1.0.0-beta.5", - "webpack-sources": "^1.0.1", - "write-json-file": "^2.3.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==", - "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==", - "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==", - "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==" - }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "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==" - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "requires": { - "find-up": "^3.0.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "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-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "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==" - } - } - }, - "hash-sum": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", - "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==" - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "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==" - }, - "hex-color-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" - }, - "hls.js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.2.7.tgz", - "integrity": "sha512-mD4Po7Q5TPNIYX6G8sDD+RS/xfrWjMjrtp+xPw3Thw8Tq557Vn0wdXIX/Zii28F9ncUMMQPZsGkoCWFna9CZCw==" - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "hsl-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==" - }, - "hsla-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==" - }, - "html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==" - }, - "html-minifier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", - "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", - "requires": { - "camel-case": "^3.0.0", - "clean-css": "^4.2.1", - "commander": "^2.19.0", - "he": "^1.2.0", - "param-case": "^2.1.1", - "relateurl": "^0.2.7", - "uglify-js": "^3.5.1" - } - }, - "html-minifier-terser": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", - "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", - "requires": { - "camel-case": "^4.1.1", - "clean-css": "^4.2.3", - "commander": "^4.1.1", - "he": "^1.2.0", - "param-case": "^3.0.3", - "relateurl": "^0.2.7", - "terser": "^4.6.3" - }, - "dependencies": { - "camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "requires": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" - }, - "param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "requires": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - } - } - }, - "html-tags": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", - "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==" - }, - "html-webpack-plugin": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz", - "integrity": "sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==", - "requires": { - "@types/html-minifier-terser": "^5.0.0", - "@types/tapable": "^1.0.5", - "@types/webpack": "^4.41.8", - "html-minifier-terser": "^5.0.1", - "loader-utils": "^1.2.3", - "lodash": "^4.17.20", - "pretty-error": "^2.1.1", - "tapable": "^1.1.3", - "util.promisify": "1.0.0" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - } - } - }, - "htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "http-assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", - "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", - "dev": true, - "requires": { - "deep-equal": "~1.0.1", - "http-errors": "~1.8.0" - }, - "dependencies": { - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true - }, - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - } - } - } - }, - "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" - }, - "dependencies": { - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - } - } - }, - "http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-middleware": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz", - "integrity": "sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==", - "requires": { - "@types/http-proxy": "^1.17.5", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" - }, - "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==" - }, - "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" - } - }, - "icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "requires": {} - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==" - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" - }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "import-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", - "integrity": "sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg==", - "requires": { - "import-from": "^2.1.0" - } - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==" - } - } - }, - "import-from": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", - "integrity": "sha512-0vdnLL2wSGnhlRmzHJAg5JHjt1l2vYhzJ7tNLGbeVg0fse56tpGaH0uzH+r9Slej+BSXXEHvBKDEnVSLLE9/+w==", - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==" - } - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" - }, - "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==" - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==" - }, - "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==" - }, - "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==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - } - }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" - }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==" - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "requires": { - "has-bigints": "^1.0.1" - } - }, - "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==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" - }, - "is-color-stop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", - "integrity": "sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA==", - "requires": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - } - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "requires": { - "has": "^1.0.3" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==" - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" - }, - "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-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" - }, - "is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==" - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" - }, - "is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==" - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-ssh": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", - "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", - "requires": { - "protocols": "^2.0.1" - } - }, - "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-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-url-superb": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz", - "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==", - "dev": true - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "requires": { - "call-bind": "^1.0.2" - } - }, - "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==" - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "requires": { - "is-docker": "^2.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" - }, - "jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - } - }, - "jimp-compact": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/jimp-compact/-/jimp-compact-0.16.1.tgz", - "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==", - "dev": true - }, - "jiti": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.16.0.tgz", - "integrity": "sha512-L3BJStEf5NAqNuzrpfbN71dp43mYIcBUlCRea/vdyv5dW/AYa1d4bpelko4SHdY3I6eN9Wzyasxirj1/vv5kmg==" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "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==", - "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==" - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, - "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 - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "requires": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", - "dev": true, - "requires": { - "tsscmp": "1.0.6" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - }, - "klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==" - }, - "koa": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.4.tgz", - "integrity": "sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==", - "dev": true, - "requires": { - "accepts": "^1.3.5", - "cache-content-type": "^1.0.0", - "content-disposition": "~0.5.2", - "content-type": "^1.0.4", - "cookies": "~0.8.0", - "debug": "^4.3.2", - "delegates": "^1.0.0", - "depd": "^2.0.0", - "destroy": "^1.0.4", - "encodeurl": "^1.0.2", - "escape-html": "^1.0.3", - "fresh": "~0.5.2", - "http-assert": "^1.3.0", - "http-errors": "^1.6.3", - "is-generator-function": "^1.0.7", - "koa-compose": "^4.1.0", - "koa-convert": "^2.0.0", - "on-finished": "^2.3.0", - "only": "~0.0.2", - "parseurl": "^1.3.2", - "statuses": "^1.5.0", - "type-is": "^1.6.16", - "vary": "^1.1.2" - }, - "dependencies": { - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "dependencies": { - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true - } - } - } - } - }, - "koa-compose": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", - "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", - "dev": true - }, - "koa-convert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", - "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", - "dev": true, - "requires": { - "co": "^4.6.0", - "koa-compose": "^4.1.0" - } - }, - "koa-send": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", - "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "http-errors": "^1.7.3", - "resolve-path": "^1.4.0" - }, - "dependencies": { - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true - }, - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - } - } - } - }, - "koa-static": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", - "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "koa-send": "^5.0.0" - }, - "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" - } - } - } - }, - "last-call-webpack-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", - "integrity": "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==", - "requires": { - "lodash": "^4.17.5", - "webpack-sources": "^1.1.0" - } - }, - "launch-editor": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", - "requires": { - "picocolors": "^1.0.0", - "shell-quote": "^1.7.3" - } - }, - "launch-editor-middleware": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor-middleware/-/launch-editor-middleware-2.6.0.tgz", - "integrity": "sha512-K2yxgljj5TdCeRN1lBtO3/J26+AIDDDw+04y6VAiZbWcTdBwsYN6RrZBnW5DN/QiSIdKNjKdATLUUluWWFYTIA==", - "requires": { - "launch-editor": "^2.6.0" - } - }, - "libarchive.js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/libarchive.js/-/libarchive.js-1.3.0.tgz", - "integrity": "sha512-EkQfRXt9DhWwj6BnEA2TNpOf4jTnzSTUPGgE+iFxcdNqjktY8GitbDeHnx8qZA0/IukNyyBUR3oQKRdYkO+HFg==" - }, - "lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "requires": { - "immediate": "~3.0.5" - } - }, - "lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", - "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 - }, - "loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==" - }, - "loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "requires": { - "lie": "3.1.1" - }, - "dependencies": { - "lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "requires": { - "immediate": "~3.0.5" - } - } - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "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._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==" - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==" - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" - }, - "lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "requires": { - "lodash._reinterpolate": "^3.0.0" - } - }, - "lodash.topath": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", - "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" - }, - "lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==" - }, - "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==", - "requires": { - "yallist": "^3.0.2" - } - }, - "luxon": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", - "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==" - }, - "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.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", - "requires": { - "object-visit": "^1.0.0" - } - }, - "marks-pane": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/marks-pane/-/marks-pane-1.0.9.tgz", - "integrity": "sha512-Ahs4oeG90tbdPWwAJkAAoHg2lRR8lAs9mZXETNPO9hYg3AkjUJBKi1NQ4aaIQZVGrig7c/3NUV1jANl8rFTeMg==" - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" - }, - "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==", - "dev": true - }, - "mem": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", - "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", - "requires": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^3.1.0" - }, - "dependencies": { - "mimic-fn": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", - "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==" - } - } - }, - "memfs": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.11.tgz", - "integrity": "sha512-GvsCITGAyDCxxsJ+X6prJexFQEhOCJaIlUbsAvjzSI5o5O7j2dle3jWvz5Z5aOdpOxW6ol3vI1+0ut+641F1+w==", - "requires": { - "fs-monkey": "^1.0.3" - } - }, - "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "requires": { - "source-map": "^0.6.1" - } - }, - "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==" - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" - }, - "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" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" - }, - "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" - } - }, - "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" - }, - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "requires": { - "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "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==", - "requires": { - "minipass": "^3.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==", - "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==", - "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" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "requires": { - "minimist": "^1.2.6" - } - }, - "modern-normalize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", - "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==", - "dev": true - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==", - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - } - }, - "mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "mustache": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz", - "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==" - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" - }, - "nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", - "optional": true - }, - "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" - }, - "no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "requires": { - "lower-case": "^1.1.1" - } - }, - "node-emoji": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", - "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", - "dev": true, - "requires": { - "lodash": "^4.17.21" - } - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "node-html-parser": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-3.3.6.tgz", - "integrity": "sha512-VkWDHvNgFGB3mbQGMyzqRE1i/BG7TKX9wRXC8e/v8kL0kZR/Oy6RjYxXH91K6/+m3g8iQ8dTqRy75lTYoA2Cjg==", - "requires": { - "css-select": "^4.1.3", - "he": "1.2.0" - } - }, - "node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - }, - "dependencies": { - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" - } - } - }, - "node-object-hash": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/node-object-hash/-/node-object-hash-1.4.2.tgz", - "integrity": "sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ==" - }, - "node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" - }, - "node-res": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/node-res/-/node-res-5.0.1.tgz", - "integrity": "sha512-YOleO9c7MAqoHC+Ccu2vzvV1fL6Ku49gShq3PIMKWHRgrMSih3XcwL05NbLBi6oU2J471gTBfdpVVxwT6Pfhxg==", - "requires": { - "destroy": "^1.0.4", - "etag": "^1.8.1", - "mime-types": "^2.1.19", - "on-finished": "^2.3.0", - "vary": "^1.1.2" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" - }, - "normalize-url": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==", - "requires": { - "object-assign": "^4.0.1", - "prepend-http": "^1.0.0", - "query-string": "^4.1.0", - "sort-keys": "^1.0.0" - } - }, - "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==", - "requires": { - "path-key": "^3.0.0" - } - }, - "nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "requires": { - "boolbase": "^1.0.0" - } - }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==" - }, - "nuxt": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-2.15.8.tgz", - "integrity": "sha512-ceK3qLg/Baj7J8mK9bIxqw9AavrF+LXqwYEreBdY/a4Sj8YV4mIvhqea/6E7VTCNNGvKT2sJ/TTJjtfQ597lTA==", - "requires": { - "@nuxt/babel-preset-app": "2.15.8", - "@nuxt/builder": "2.15.8", - "@nuxt/cli": "2.15.8", - "@nuxt/components": "^2.1.8", - "@nuxt/config": "2.15.8", - "@nuxt/core": "2.15.8", - "@nuxt/generator": "2.15.8", - "@nuxt/loading-screen": "^2.0.3", - "@nuxt/opencollective": "^0.3.2", - "@nuxt/server": "2.15.8", - "@nuxt/telemetry": "^1.3.3", - "@nuxt/utils": "2.15.8", - "@nuxt/vue-app": "2.15.8", - "@nuxt/vue-renderer": "2.15.8", - "@nuxt/webpack": "2.15.8" - } - }, - "nuxt-socket-io": { - "version": "1.1.24", - "resolved": "https://registry.npmjs.org/nuxt-socket-io/-/nuxt-socket-io-1.1.24.tgz", - "integrity": "sha512-ax9Q9ye0i+yrw/VBLGZVZA2WOj/qdRq7O9i8p6eMX7b+SCqa/7FSFdWTJsOoAIDZ9gPFyF+dK19h8pg/h/bCEg==", - "requires": { - "@nuxt/types": "^2.15.5", - "glob": "^7.1.7", - "socket.io": "^4.1.1", - "socket.io-client": "^4.1.1", - "tiny-emitter": "^2.1.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-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true - }, - "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", - "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", - "requires": { - "array.prototype.reduce": "^1.0.5", - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "requires": { - "isobject": "^3.0.1" - } - }, - "object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "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" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "only": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", - "dev": true - }, - "open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, - "requires": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - } - }, - "opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==" - }, - "optimize-css-assets-webpack-plugin": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.8.tgz", - "integrity": "sha512-mgFS1JdOtEGzD8l+EuISqL57cKO+We9GcoiQEmdCWRqqck+FGNmYJtx9qfAPzEz+lRrlThWMuGDaRkI/yWNx/Q==", - "requires": { - "cssnano": "^4.1.10", - "last-call-webpack-plugin": "^3.0.0" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==" - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.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==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "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==", - "requires": { - "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==" - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "parallel-transform": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", - "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", - "requires": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", - "requires": { - "no-case": "^2.2.0" - } - }, - "parent-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-2.0.0.tgz", - "integrity": "sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg==", - "dev": true, - "requires": { - "callsites": "^3.1.0" - }, - "dependencies": { - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - } - } - }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "parse-git-config": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-3.0.0.tgz", - "integrity": "sha512-wXoQGL1D+2COYWCD35/xbiKma1Z15xvZL8cI25wvxzled58V51SJM04Urt/uznS900iQor7QO04SgdfT/XlbuA==", - "requires": { - "git-config-path": "^2.0.0", - "ini": "^1.3.5" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "parse-path": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", - "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", - "requires": { - "protocols": "^2.0.0" - } - }, - "parse-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", - "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", - "requires": { - "parse-path": "^7.0.0" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - }, - "dependencies": { - "lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "requires": { - "tslib": "^2.0.3" - } - }, - "no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "requires": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - } - } - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==" - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", - "optional": true - }, - "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==" - }, - "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==" - }, - "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==" - }, - "path-to-regexp": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" - }, - "path-webpack": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/path-webpack/-/path-webpack-0.0.3.tgz", - "integrity": "sha512-AmeDxedoo5svf7aB3FYqSAKqMxys014lVKBzy1o/5vv9CtU7U4wgGWL1dA2o6MOzcD53ScN4Jmiq6VbtLz1vIQ==" - }, - "pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "pdfjs-dist": { - "version": "2.6.347", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.6.347.tgz", - "integrity": "sha512-QC+h7hG2su9v/nU1wEI3SnpPIrqJODL7GTDFvR74ANKGq1AFJW16PH8VWnhpiTi9YcLSFV9xLeWSgq+ckHLdVQ==" - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" - }, - "pify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", - "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" - }, - "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==", - "requires": { - "find-up": "^4.0.0" - } - }, - "pnp-webpack-plugin": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz", - "integrity": "sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==", - "requires": { - "ts-pnp": "^1.1.6" - } - }, - "portfinder": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", - "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", - "dev": true, - "requires": { - "async": "^2.6.4", - "debug": "^3.2.7", - "mkdirp": "^0.5.6" - }, - "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" - } - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==" - }, - "postcss": { - "version": "8.4.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", - "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==", - "requires": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-attribute-case-insensitive": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz", - "integrity": "sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA==", - "requires": { - "postcss": "^7.0.2", - "postcss-selector-parser": "^6.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-calc": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", - "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", - "requires": { - "postcss": "^7.0.27", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-color-functional-notation": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz", - "integrity": "sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==", - "requires": { - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-color-gray": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz", - "integrity": "sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==", - "requires": { - "@csstools/convert-colors": "^1.4.0", - "postcss": "^7.0.5", - "postcss-values-parser": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-color-hex-alpha": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz", - "integrity": "sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw==", - "requires": { - "postcss": "^7.0.14", - "postcss-values-parser": "^2.0.1" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-color-mod-function": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz", - "integrity": "sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==", - "requires": { - "@csstools/convert-colors": "^1.4.0", - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-color-rebeccapurple": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz", - "integrity": "sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==", - "requires": { - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-colormin": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", - "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", - "requires": { - "browserslist": "^4.0.0", - "color": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-convert-values": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", - "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-custom-media": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz", - "integrity": "sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg==", - "requires": { - "postcss": "^7.0.14" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-custom-properties": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-11.0.0.tgz", - "integrity": "sha512-Fhnx/QLt+CTt23A/KKVx1anZD9nmVpOxKCKv5owWacMoOsBXFhMAD6SZYbmPMH4nHdIeMUnWOvLZnlY4niS0sA==", - "dev": true, - "requires": { - "postcss-values-parser": "^4.0.0" - } - }, - "postcss-custom-selectors": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz", - "integrity": "sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==", - "requires": { - "postcss": "^7.0.2", - "postcss-selector-parser": "^5.0.0-rc.3" - }, - "dependencies": { - "cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==" - }, - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "requires": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-dir-pseudo-class": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz", - "integrity": "sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==", - "requires": { - "postcss": "^7.0.2", - "postcss-selector-parser": "^5.0.0-rc.3" - }, - "dependencies": { - "cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==" - }, - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "requires": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-discard-comments": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", - "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-discard-duplicates": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", - "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-discard-empty": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", - "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-discard-overridden": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", - "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-double-position-gradients": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz", - "integrity": "sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==", - "requires": { - "postcss": "^7.0.5", - "postcss-values-parser": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-env-function": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-2.0.2.tgz", - "integrity": "sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==", - "requires": { - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-focus-visible": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz", - "integrity": "sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==", - "requires": { - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-focus-within": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz", - "integrity": "sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==", - "requires": { - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-font-variant": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz", - "integrity": "sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA==", - "requires": { - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-gap-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz", - "integrity": "sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==", - "requires": { - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-image-set-function": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz", - "integrity": "sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==", - "requires": { - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-import": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-13.0.0.tgz", - "integrity": "sha512-LPUbm3ytpYopwQQjqgUH4S3EM/Gb9QsaSPP/5vnoi+oKVy3/mIk2sc0Paqw7RL57GpScm9MdIMUypw2znWiBpg==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - } - }, - "postcss-import-resolver": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-import-resolver/-/postcss-import-resolver-2.0.0.tgz", - "integrity": "sha512-y001XYgGvVwgxyxw9J1a5kqM/vtmIQGzx34g0A0Oy44MFcy/ZboZw1hu/iN3VYFjSTRzbvd7zZJJz0Kh0AGkTw==", - "requires": { - "enhanced-resolve": "^4.1.1" - } - }, - "postcss-initial": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.4.tgz", - "integrity": "sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg==", - "requires": { - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", - "dev": true, - "requires": { - "camelcase-css": "^2.0.1" - } - }, - "postcss-lab-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz", - "integrity": "sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==", - "requires": { - "@csstools/convert-colors": "^1.4.0", - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "requires": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - } - }, - "postcss-loader": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-4.3.0.tgz", - "integrity": "sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q==", - "dev": true, - "requires": { - "cosmiconfig": "^7.0.0", - "klona": "^2.0.4", - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.4" - }, - "dependencies": { - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "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" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } - } - }, - "postcss-logical": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-3.0.0.tgz", - "integrity": "sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==", - "requires": { - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-media-minmax": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz", - "integrity": "sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==", - "requires": { - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-merge-longhand": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", - "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", - "requires": { - "css-color-names": "0.0.4", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "stylehacks": "^4.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-merge-rules": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", - "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "cssnano-util-same-parent": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0", - "vendors": "^1.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-minify-font-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", - "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-minify-gradients": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", - "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "is-color-stop": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-minify-params": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", - "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", - "requires": { - "alphanum-sort": "^1.0.0", - "browserslist": "^4.0.0", - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "uniqs": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-minify-selectors": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", - "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", - "requires": { - "alphanum-sort": "^1.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "requires": {} - }, - "postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "requires": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - } - }, - "postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "requires": { - "postcss-selector-parser": "^6.0.4" - } - }, - "postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "requires": { - "icss-utils": "^5.0.0" - } - }, - "postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.10" - } - }, - "postcss-nesting": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-8.0.1.tgz", - "integrity": "sha512-cHPNhW5VvRQjszFDxmy16mis9qFQqQLBNw6KVmueLqqE3M182ZAk9+QoxGqbGVryzLVhannw2B5Yhosqq522fA==", - "dev": true, - "requires": {} - }, - "postcss-normalize-charset": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", - "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-normalize-display-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", - "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-normalize-positions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", - "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-normalize-repeat-style": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", - "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-normalize-string": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", - "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", - "requires": { - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-normalize-timing-functions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", - "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-normalize-unicode": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", - "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-normalize-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", - "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", - "requires": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==" - }, - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-normalize-whitespace": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", - "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-ordered-values": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", - "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-overflow-shorthand": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz", - "integrity": "sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==", - "requires": { - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-page-break": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-2.0.0.tgz", - "integrity": "sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==", - "requires": { - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-place": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-4.0.1.tgz", - "integrity": "sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==", - "requires": { - "postcss": "^7.0.2", - "postcss-values-parser": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-preset-env": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.7.1.tgz", - "integrity": "sha512-rlRkgX9t0v2On33n7TK8pnkcYOATGQSv48J2RS8GsXhqtg+xk6AummHP88Y5mJo0TLJelBjePvSjScTNkj3+qw==", - "requires": { - "autoprefixer": "^9.6.1", - "browserslist": "^4.6.4", - "caniuse-lite": "^1.0.30000981", - "css-blank-pseudo": "^0.1.4", - "css-has-pseudo": "^0.10.0", - "css-prefers-color-scheme": "^3.1.1", - "cssdb": "^4.4.0", - "postcss": "^7.0.17", - "postcss-attribute-case-insensitive": "^4.0.1", - "postcss-color-functional-notation": "^2.0.1", - "postcss-color-gray": "^5.0.0", - "postcss-color-hex-alpha": "^5.0.3", - "postcss-color-mod-function": "^3.0.3", - "postcss-color-rebeccapurple": "^4.0.1", - "postcss-custom-media": "^7.0.8", - "postcss-custom-properties": "^8.0.11", - "postcss-custom-selectors": "^5.1.2", - "postcss-dir-pseudo-class": "^5.0.0", - "postcss-double-position-gradients": "^1.0.0", - "postcss-env-function": "^2.0.2", - "postcss-focus-visible": "^4.0.0", - "postcss-focus-within": "^3.0.0", - "postcss-font-variant": "^4.0.0", - "postcss-gap-properties": "^2.0.0", - "postcss-image-set-function": "^3.0.1", - "postcss-initial": "^3.0.0", - "postcss-lab-function": "^2.0.1", - "postcss-logical": "^3.0.0", - "postcss-media-minmax": "^4.0.0", - "postcss-nesting": "^7.0.0", - "postcss-overflow-shorthand": "^2.0.0", - "postcss-page-break": "^2.0.0", - "postcss-place": "^4.0.1", - "postcss-pseudo-class-any-link": "^6.0.0", - "postcss-replace-overflow-wrap": "^3.0.0", - "postcss-selector-matches": "^4.0.0", - "postcss-selector-not": "^4.0.0" - }, - "dependencies": { - "autoprefixer": { - "version": "9.8.8", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", - "integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==", - "requires": { - "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001109", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "picocolors": "^0.2.1", - "postcss": "^7.0.32", - "postcss-value-parser": "^4.1.0" - } - }, - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-custom-properties": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz", - "integrity": "sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA==", - "requires": { - "postcss": "^7.0.17", - "postcss-values-parser": "^2.0.1" - } - }, - "postcss-nesting": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-7.0.1.tgz", - "integrity": "sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg==", - "requires": { - "postcss": "^7.0.2" - } - }, - "postcss-values-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", - "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-pseudo-class-any-link": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz", - "integrity": "sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==", - "requires": { - "postcss": "^7.0.2", - "postcss-selector-parser": "^5.0.0-rc.3" - }, - "dependencies": { - "cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==" - }, - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "requires": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-reduce-initial": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", - "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-reduce-transforms": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", - "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", - "requires": { - "cssnano-util-get-match": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-replace-overflow-wrap": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz", - "integrity": "sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==", - "requires": { - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-selector-matches": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz", - "integrity": "sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==", - "requires": { - "balanced-match": "^1.0.0", - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-selector-not": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz", - "integrity": "sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ==", - "requires": { - "balanced-match": "^1.0.0", - "postcss": "^7.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-svgo": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz", - "integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==", - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "svgo": "^1.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, - "postcss-unique-selectors": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", - "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", - "requires": { - "alphanum-sort": "^1.0.0", - "postcss": "^7.0.0", - "uniqs": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-url": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-10.1.3.tgz", - "integrity": "sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==", - "dev": true, - "requires": { - "make-dir": "~3.1.0", - "mime": "~2.5.2", - "minimatch": "~3.0.4", - "xxhashjs": "~0.2.2" - }, - "dependencies": { - "minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "postcss-values-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-4.0.0.tgz", - "integrity": "sha512-R9x2D87FcbhwXUmoCXJR85M1BLII5suXRuXibGYyBJ7lVDEpRIdKZh4+8q5S+/+A4m0IoG1U5tFw39asyhX/Hw==", - "dev": true, - "requires": { - "color-name": "^1.1.4", - "is-url-superb": "^4.0.0", - "postcss": "^7.0.5" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==" - }, - "prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "optional": true - }, - "pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==" - }, - "pretty-error": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", - "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", - "requires": { - "lodash": "^4.17.20", - "renderkid": "^2.0.4" - } - }, - "pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", - "dev": true - }, - "pretty-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", - "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==" - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==" - }, - "proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "requires": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "protocols": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", - "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==" - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==" - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "purgecss": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.1.3.tgz", - "integrity": "sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==", - "dev": true, - "requires": { - "commander": "^8.0.0", - "glob": "^7.1.7", - "postcss": "^8.3.5", - "postcss-selector-parser": "^6.0.6" - }, - "dependencies": { - "commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true - } - } - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==" - }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", - "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==" - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" - }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "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-loader": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", - "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - } - }, - "rc9": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-1.2.4.tgz", - "integrity": "sha512-YD1oJO9LUzMdmr2sAsVlwQVtEoDCmvuyDwmSWrg2GKFprl3BckP5cmw9rHPunei0lV6Xl4E5t2esT+0trY1xfQ==", - "requires": { - "defu": "^6.0.0", - "destr": "^1.1.1", - "flat": "^5.0.0" - }, - "dependencies": { - "defu": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.1.tgz", - "integrity": "sha512-aA964RUCsBt0FGoNIlA3uFgo2hO+WWC0fiC6DBps/0SFzkKcYoM/3CzVLIa5xSsrFjdioMdYgAIbwo80qp2MoA==" - } - } - }, - "read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "requires": { - "pify": "^2.3.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" - } - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "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==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "reduce-css-calc": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", - "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", - "dev": true, - "requires": { - "css-unit-converter": "^1.1.1", - "postcss-value-parser": "^3.3.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "requires": { - "regenerate": "^1.4.2" - } - }, - "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, - "regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "regexpu-core": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", - "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", - "requires": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsgen": "^0.7.1", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - } - }, - "regjsgen": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", - "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==" - }, - "regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" - } - } - }, - "relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==" - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "optional": true - }, - "renderkid": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", - "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", - "requires": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==" - }, - "replace-in-file": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.5.tgz", - "integrity": "sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==", - "dev": true, - "requires": { - "chalk": "^4.1.2", - "glob": "^7.2.0", - "yargs": "^17.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 - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.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-path": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", - "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", - "dev": true, - "requires": { - "http-errors": "~1.6.2", - "path-is-absolute": "1.0.1" - }, - "dependencies": { - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - } - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==" - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==" - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" - }, - "rgb-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w==" - }, - "rgba-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==" - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==", - "requires": { - "aproba": "^1.1.1" - } - }, - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "requires": { - "tslib": "^1.9.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", - "requires": { - "ret": "~0.1.10" - } - }, - "safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sass-loader": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.1.1.tgz", - "integrity": "sha512-W6gVDXAd5hR/WHsPicvZdjAWHBcEJ44UahgxcIE196fW2ong0ZHMPO1kZuI5q0VlvMQZh32gpv69PLWQm70qrw==", - "requires": { - "klona": "^2.0.4", - "loader-utils": "^2.0.0", - "neo-async": "^2.6.2", - "schema-utils": "^3.0.0", - "semver": "^7.3.2" - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, - "scule": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/scule/-/scule-0.2.1.tgz", - "integrity": "sha512-M9gnWtn3J0W+UhJOHmBxBTwv8mZCan5i1Himp60t6vvZcor0wr+IM0URKmIglsWJ7bRujNAVVN77fp+uZaWoKg==" - }, - "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" - }, - "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" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "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": { - "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" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "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" - } - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - } - } - }, - "serialize-javascript": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", - "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", - "requires": { - "randombytes": "^2.1.0" - } - }, - "serve-placeholder": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/serve-placeholder/-/serve-placeholder-1.2.4.tgz", - "integrity": "sha512-jWD9cZXLcr4vHTTL5KEPIUBUYyOWN/z6v/tn0l6XxFhi9iqV3Fc5Y1aFeduUyz+cx8sALzGCUczkPfeOlrq9jg==", - "requires": { - "defu": "^5.0.0" - }, - "dependencies": { - "defu": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/defu/-/defu-5.0.1.tgz", - "integrity": "sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==" - } - } - }, - "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" - } - }, - "server-destroy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", - "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==" - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "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==", - "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==" - }, - "shell-quote": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", - "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==" - }, - "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-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } - } - }, - "sirv": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", - "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", - "requires": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^1.0.0" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "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" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "socket.io": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz", - "integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==", - "requires": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "debug": "~4.3.2", - "engine.io": "~6.2.0", - "socket.io-adapter": "~2.4.0", - "socket.io-parser": "~4.2.0" - } - }, - "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-client": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.3.tgz", - "integrity": "sha512-I/hqDYpQ6JKwtJOf5ikM+Qz+YujZPMEl6qBLhxiP0nX+TfXKhW4KZZG8lamrD6Y5ngjmYHreESVasVCgi5Kl3A==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.2.3", - "socket.io-parser": "~4.2.0" - } - }, - "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" - } - }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", - "requires": { - "is-plain-obj": "^1.0.0" - }, - "dependencies": { - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==" - } - } - }, - "sortablejs": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz", - "integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==" - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==" - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "requires": { - "minipass": "^3.1.1" - } - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" - }, - "stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" - }, - "std-env": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-2.3.1.tgz", - "integrity": "sha512-eOsoKTWnr6C8aWrqJJ2KAReXoa7Vn5Ywyw6uCXgA/xDhxPoaIsBa5aNJmISY04dLwXPBnDHW4diGM7Sn5K4R/g==", - "requires": { - "ci-info": "^3.1.1" - } - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.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" - } - }, - "string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "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" - } - }, - "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==" - }, - "style-resources-loader": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/style-resources-loader/-/style-resources-loader-1.5.0.tgz", - "integrity": "sha512-fIfyvQ+uvXaCBGGAgfh+9v46ARQB1AWdaop2RpQw0PBVuROsTBqGvx8dj0kxwjGOAyq3vepe4AOK3M6+Q/q2jw==", - "requires": { - "glob": "^7.2.0", - "loader-utils": "^2.0.0", - "schema-utils": "^2.7.0", - "tslib": "^2.3.1" - }, - "dependencies": { - "schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "requires": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - } - }, - "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - } - } - }, - "stylehacks": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", - "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "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==", - "requires": { - "has-flag": "^4.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==" - }, - "svg-tags": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==" - }, - "svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "requires": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.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==", - "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==", - "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==", - "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==" - }, - "css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "requires": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==" - }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - }, - "dependencies": { - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" - } - } - }, - "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==" - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "requires": { - "boolbase": "~1.0.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "tailwind-config-viewer": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/tailwind-config-viewer/-/tailwind-config-viewer-1.7.2.tgz", - "integrity": "sha512-3JJCeAAlvG+i/EBj+tQb0x4weo30QjdSAo4hlcnVbtD+CkpzHi/UwU9InbPMcYH+ESActoa2kCyjpLEyjEkn0Q==", - "dev": true, - "requires": { - "@koa/router": "^9.0.1", - "commander": "^6.0.0", - "fs-extra": "^9.0.1", - "koa": "^2.12.0", - "koa-static": "^5.0.0", - "open": "^7.0.4", - "portfinder": "^1.0.26", - "replace-in-file": "^6.1.0" - }, - "dependencies": { - "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true - } - } - }, - "tailwindcss": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz", - "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==", - "dev": true, - "requires": { - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "color-name": "^1.1.4", - "detective": "^5.2.1", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.12", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "lilconfig": "^2.0.6", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.18", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", - "postcss-selector-parser": "^6.0.10", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1" - }, - "dependencies": { - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - } - } - } - }, - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" - }, - "tar": { - "version": "6.1.12", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.12.tgz", - "integrity": "sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw==", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "terser": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", - "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - } - }, - "terser-webpack-plugin": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-4.2.3.tgz", - "integrity": "sha512-jTgXh40RnvOrLQNgIkwEKnQ8rmHjHK4u+6UBEi+W+FPmvb+uo+chJXntKe7/3lW5mNysgSWD60KyesnhW8D6MQ==", - "requires": { - "cacache": "^15.0.5", - "find-cache-dir": "^3.3.1", - "jest-worker": "^26.5.0", - "p-limit": "^3.0.2", - "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1", - "source-map": "^0.6.1", - "terser": "^5.3.4", - "webpack-sources": "^1.4.3" - }, - "dependencies": { - "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", - "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" - }, - "thread-loader": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-3.0.4.tgz", - "integrity": "sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==", - "requires": { - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^4.1.0", - "loader-utils": "^2.0.0", - "neo-async": "^2.6.2", - "schema-utils": "^3.0.0" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "time-fix-plugin": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/time-fix-plugin/-/time-fix-plugin-2.0.7.tgz", - "integrity": "sha512-uVFet1LQToeUX0rTcSiYVYVoGuBpc8gP/2jnlUzuHMHe+gux6XLsNzxLUweabMwiUj5ejhoIMsUI55nVSEa/Vw==", - "requires": {} - }, - "timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "requires": { - "setimmediate": "^1.0.4" - } - }, - "timsort": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==" - }, - "tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==" - }, - "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==" - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "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==", - "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==" - }, - "totalist": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", - "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==" - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "trix": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/trix/-/trix-1.3.1.tgz", - "integrity": "sha512-BbH6mb6gk+AV4f2as38mP6Ucc1LE3OD6XxkZnAgPIduWXYtvg2mI3cZhIZSLqmMh9OITEpOBCCk88IVmyjU7bA==" - }, - "ts-pnp": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", - "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==" - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "dev": true - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==" - }, - "type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" - }, - "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", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, - "ua-parser-js": { - "version": "0.7.32", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz", - "integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==" - }, - "ufo": { - "version": "0.7.11", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-0.7.11.tgz", - "integrity": "sha512-IT3q0lPvtkqQ8toHQN/BkOi4VIqoqheqM1FnkNWT9y0G8B3xJhwnoKBu5OHx8zHDOvveQzfKuFowJ0VSARiIDg==" - }, - "uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==" - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - }, - "unfetch": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", - "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==" - }, - "unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==" - }, - "unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "requires": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==" - }, - "unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==" - }, - "uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ==" - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "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==", - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==" - } - } - }, - "upath": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", - "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==" - }, - "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==" - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==" - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" - } - } - }, - "url-loader": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", - "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", - "requires": { - "loader-utils": "^2.0.0", - "mime-types": "^2.1.27", - "schema-utils": "^3.0.0" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" - }, - "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==" - }, - "v-click-outside": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v-click-outside/-/v-click-outside-3.2.0.tgz", - "integrity": "sha512-QD0bDy38SHJXQBjgnllmkI/rbdiwmq9RC+/+pvrFjYJKTn8dtp7Penf9q1lLBta280fYG2q53mgLhQ+3l3z74w==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, - "vendors": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", - "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==" - }, - "vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" - }, - "vue": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz", - "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==", - "requires": { - "@vue/compiler-sfc": "2.7.14", - "csstype": "^3.1.0" - } - }, - "vue-client-only": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vue-client-only/-/vue-client-only-2.1.0.tgz", - "integrity": "sha512-vKl1skEKn8EK9f8P2ZzhRnuaRHLHrlt1sbRmazlvsx6EiC3A8oWF8YCBrMJzoN+W3OnElwIGbVjsx6/xelY1AA==" - }, - "vue-hot-reload-api": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", - "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==" - }, - "vue-loader": { - "version": "15.10.1", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz", - "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==", - "requires": { - "@vue/component-compiler-utils": "^3.1.0", - "hash-sum": "^1.0.2", - "loader-utils": "^1.1.0", - "vue-hot-reload-api": "^2.3.0", - "vue-style-loader": "^4.1.0" - }, - "dependencies": { - "hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==" - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - } - } - }, - "vue-meta": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/vue-meta/-/vue-meta-2.4.0.tgz", - "integrity": "sha512-XEeZUmlVeODclAjCNpWDnjgw+t3WA6gdzs6ENoIAgwO1J1d5p1tezDhtteLUFwcaQaTtayRrsx7GL6oXp/m2Jw==", - "requires": { - "deepmerge": "^4.2.2" - } - }, - "vue-no-ssr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz", - "integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g==" - }, - "vue-resize-sensor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz", - "integrity": "sha512-W+y2EAI/BxS4Vlcca9scQv8ifeBFck56DRtSwWJ2H4Cw1GLNUYxiZxUHHkuzuI5JPW/cYtL1bPO5xPyEXx4LmQ==" - }, - "vue-router": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz", - "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==" - }, - "vue-server-renderer": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-server-renderer/-/vue-server-renderer-2.7.14.tgz", - "integrity": "sha512-NlGFn24tnUrj7Sqb8njhIhWREuCJcM3140aMunLNcx951BHG8j3XOrPP7psSCaFA8z6L4IWEjudztdwTp1CBVw==", - "requires": { - "chalk": "^4.1.2", - "hash-sum": "^2.0.0", - "he": "^1.2.0", - "lodash.template": "^4.5.0", - "lodash.uniq": "^4.5.0", - "resolve": "^1.22.0", - "serialize-javascript": "^6.0.0", - "source-map": "0.5.6" - }, - "dependencies": { - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "requires": { - "randombytes": "^2.1.0" - } - }, - "source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==" - } - } - }, - "vue-style-loader": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", - "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", - "requires": { - "hash-sum": "^1.0.2", - "loader-utils": "^1.0.2" - }, - "dependencies": { - "hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==" - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - } - } - }, - "vue-template-compiler": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", - "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", - "requires": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "vue-template-es2015-compiler": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", - "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==" - }, - "vue-toastification": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-1.7.14.tgz", - "integrity": "sha512-khZR8t3NWZ/JJ2MZxXLbesHrRJ8AKa75PY5Zq8yMifF9x8lHq8ljYkC0d2PD9yahooygQB5tcFyRDkbbIPx8hw==", - "requires": {} - }, - "vuedraggable": { - "version": "2.24.3", - "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz", - "integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==", - "requires": { - "sortablejs": "1.10.2" - } - }, - "vuex": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz", - "integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==", - "requires": {} - }, - "watchpack": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", - "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", - "requires": { - "chokidar": "^3.4.1", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0", - "watchpack-chokidar2": "^2.0.1" - } - }, - "watchpack-chokidar2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", - "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", - "optional": true, - "requires": { - "chokidar": "^2.1.8" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "optional": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "optional": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "optional": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "optional": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "optional": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "optional": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "optional": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", - "optional": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", - "optional": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "optional": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", - "optional": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "optional": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "optional": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "optional": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "optional": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "optional": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "optional": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "optional": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - } - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "optional": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "optional": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "optional": true - } - } - }, - "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==" - }, - "webpack": { - "version": "4.46.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", - "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "^6.4.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.5.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.7.4", - "webpack-sources": "^1.4.1" - }, - "dependencies": { - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==" - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "cacache": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", - "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==" - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==" - }, - "loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==", - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - } - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "requires": { - "find-up": "^3.0.0" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "requires": { - "randombytes": "^2.1.0" - } - }, - "ssri": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", - "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "terser-webpack-plugin": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", - "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - } - } - }, - "webpack-bundle-analyzer": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz", - "integrity": "sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==", - "requires": { - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", - "commander": "^7.2.0", - "gzip-size": "^6.0.0", - "lodash": "^4.17.20", - "opener": "^1.5.2", - "sirv": "^1.0.7", - "ws": "^7.3.1" - }, - "dependencies": { - "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" - }, - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" - }, - "ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "requires": {} - } - } - }, - "webpack-dev-middleware": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz", - "integrity": "sha512-PjwyVY95/bhBh6VUqt6z4THplYcsvQ8YNNBTBM873xLVmw8FLeALn0qurHbs9EmcfhzQis/eoqypSnZeuUz26w==", - "requires": { - "colorette": "^1.2.2", - "mem": "^8.1.1", - "memfs": "^3.2.2", - "mime-types": "^2.1.30", - "range-parser": "^1.2.1", - "schema-utils": "^3.0.0" - } - }, - "webpack-hot-middleware": { - "version": "2.25.3", - "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.25.3.tgz", - "integrity": "sha512-IK/0WAHs7MTu1tzLTjio73LjS3Ov+VvBKQmE8WPlJutgG5zT6Urgq/BbAdRrHTRpyzK0dvAvFh1Qg98akxgZpA==", - "requires": { - "ansi-html-community": "0.0.8", - "html-entities": "^2.1.0", - "strip-ansi": "^6.0.0" - } - }, - "webpack-node-externals": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", - "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==" - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "webpackbar": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-4.0.0.tgz", - "integrity": "sha512-k1qRoSL/3BVuINzngj09nIwreD8wxV4grcuhHTD8VJgUbGcy8lQSPqv+bM00B7F+PffwIsQ8ISd4mIwRbr23eQ==", - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", - "consola": "^2.10.0", - "figures": "^3.0.0", - "pretty-time": "^1.1.0", - "std-env": "^2.2.1", - "text-table": "^0.2.0", - "wrap-ansi": "^6.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.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==", - "requires": { - "color-convert": "^1.9.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==", - "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==" - }, - "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==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "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==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } - } - }, - "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==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "requires": { - "string-width": "^4.0.0" - } - }, - "workbox-cdn": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/workbox-cdn/-/workbox-cdn-5.1.4.tgz", - "integrity": "sha512-04gM3mi8QGutokkSaA9xunVfjURnLbo9TTWyi8+pSDCEW5cD8u5GbJiliLK1vB9CShk/9OY1UDfW+XcmD+d6KQ==", - "dev": true - }, - "worker-farm": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", - "requires": { - "errno": "~0.1.7" - } - }, - "worker-loader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz", - "integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==", - "requires": { - "loader-utils": "^1.0.0", - "schema-utils": "^0.4.0" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", - "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "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==", - "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": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "write-json-file": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-2.3.0.tgz", - "integrity": "sha512-84+F0igFp2dPD6UpAQjOUX3CdKUOqUzn6oE9sDBNzUXINR5VceJ1rauZltqQB/bcYsx3EpKys4C7/PivKUAiWQ==", - "requires": { - "detect-indent": "^5.0.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "pify": "^3.0.0", - "sort-keys": "^2.0.0", - "write-file-atomic": "^2.0.0" - }, - "dependencies": { - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==" - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==" - }, - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", - "requires": { - "is-plain-obj": "^1.0.0" - } - } - } - }, - "ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", - "requires": {} - }, - "xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, - "xxhashjs": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", - "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", - "requires": { - "cuint": "^0.2.2" - } - }, - "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": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true - }, - "yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", - "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 - }, - "ylru": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.3.2.tgz", - "integrity": "sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==", - "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==" - } } -} \ No newline at end of file +} diff --git a/client/package.json b/client/package.json index 2195f320..6a852b03 100644 --- a/client/package.json +++ b/client/package.json @@ -23,7 +23,7 @@ "epubjs": "^0.3.88", "hls.js": "^1.0.7", "libarchive.js": "^1.3.0", - "nuxt": "^2.15.8", + "nuxt": "^2.17.3", "nuxt-socket-io": "^1.1.18", "trix": "^1.3.1", "v-click-outside": "^3.1.2", @@ -31,11 +31,9 @@ "vuedraggable": "^2.24.3" }, "devDependencies": { - "@nuxt/postcss8": "^1.1.3", "@nuxtjs/pwa": "^3.3.5", - "@nuxtjs/tailwindcss": "^4.2.1", "autoprefixer": "^10.4.7", "postcss": "^8.3.6", - "tailwindcss": "^3.1.4" + "tailwindcss": "^3.4.1" } -} \ No newline at end of file +} diff --git a/client/tailwind.config.js b/client/tailwind.config.js index 9eb81923..e081f5c5 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -1,29 +1,18 @@ module.exports = { - purge: { - content: [ - 'components/**/*.vue', - 'layouts/**/*.vue', - 'pages/**/*.vue', - 'templates/**/*.vue', - 'plugins/**/*.js', - 'nuxt.config.js' - ], - safelist: [ - 'bg-success', - 'bg-red-600', - 'bg-yellow-400', - 'text-green-500', - 'py-1.5', - 'bg-info', - 'px-1.5', - 'min-w-5', - 'w-3.5', - 'h-3.5', - 'border-warning', - 'mb-px', - 'text-1.5xl' - ], - }, + content: [ + 'components/**/*.vue', + 'layouts/**/*.vue', + 'pages/**/*.vue', + 'templates/**/*.vue', + 'plugins/**/*.js', + 'nuxt.config.js' + ], + safelist: [ + 'bg-red-600', + 'px-1.5', + 'min-w-5', + 'border-warning' + ], theme: { extend: { height: { diff --git a/package-lock.json b/package-lock.json index 0e323ca3..ac2d4cc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "audiobookshelf", "version": "2.7.2", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -21,7 +21,7 @@ "openid-client": "^5.6.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", - "sequelize": "^6.32.1", + "sequelize": "^6.35.2", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", "ssrf-req-filter": "^1.1.0", @@ -52,12 +52,12 @@ } }, "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==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -105,31 +105,61 @@ "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/code-frame/node_modules/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, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/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, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "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==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "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==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", + "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", "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/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", + "@babel/helpers": "^7.23.7", + "@babel/parser": "^7.23.6", "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -183,12 +213,12 @@ } }, "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==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.23.3", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -198,14 +228,14 @@ } }, "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==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -327,9 +357,9 @@ } }, "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==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -345,32 +375,32 @@ } }, "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==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "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==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz", + "integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==", "dev": true, "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" }, "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==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -422,10 +452,40 @@ "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/highlight/node_modules/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, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/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, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "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==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -449,20 +509,20 @@ } }, "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==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", "@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", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -493,12 +553,12 @@ "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==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, @@ -528,6 +588,80 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -576,9 +710,9 @@ "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==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -586,9 +720,9 @@ } }, "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", - "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "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==", "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", @@ -604,6 +738,37 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/node-pre-gyp/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", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "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" + }, + "engines": { + "node": ">=10" + } + }, "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", @@ -615,24 +780,21 @@ "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", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" } }, "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "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" }, @@ -666,9 +828,9 @@ } }, "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "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, "dependencies": { "lru-cache": "^6.0.0" @@ -704,9 +866,9 @@ } }, "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==", + "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" @@ -758,35 +920,38 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "node_modules/@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dependencies": { "@types/node": "*" } }, "node_modules/@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==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "dependencies": { "@types/ms": "*" } }, "node_modules/@types/ms": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", - "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + "version": "20.11.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", + "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/validator": { - "version": "13.7.17", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz", - "integrity": "sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==" + "version": "13.11.8", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz", + "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, "node_modules/abbrev": { "version": "1.1.1", @@ -838,42 +1003,17 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/agentkeepalive": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", - "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "optional": true, "dependencies": { - "debug": "^4.1.0", - "depd": "^2.0.0", "humanize-ms": "^1.2.1" }, "engines": { "node": ">= 8.0.0" } }, - "node_modules/agentkeepalive/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "optional": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agentkeepalive/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==", - "optional": true - }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -956,25 +1096,23 @@ "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", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "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, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" }, "engines": { - "node": ">=10" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "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" - } + "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/array-flatten": { "version": "1.1.1", @@ -1050,12 +1188,12 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -1077,9 +1215,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "funding": [ { @@ -1096,9 +1234,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -1162,6 +1300,21 @@ "node": ">=10" } }, + "node_modules/cacache/node_modules/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, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caching-transform": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", @@ -1178,12 +1331,13 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1199,9 +1353,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001561", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", - "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "version": "1.0.30001579", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", + "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", "dev": true, "funding": [ { @@ -1219,9 +1373,9 @@ ] }, "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==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", @@ -1252,15 +1406,6 @@ "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", @@ -1330,28 +1475,14 @@ } }, "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "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": "^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" + "wrap-ansi": "^7.0.0" } }, "node_modules/color-convert": { @@ -1419,9 +1550,9 @@ } }, "node_modules/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==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "engines": { "node": ">= 0.6" } @@ -1433,9 +1564,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "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" } @@ -1452,14 +1583,6 @@ "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", @@ -1500,15 +1623,12 @@ } }, "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==", + "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": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/deep-eql": { @@ -1538,6 +1658,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1569,9 +1702,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "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==", "engines": { "node": ">=8" } @@ -1624,13 +1757,13 @@ } }, "node_modules/domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" + "domhandler": "^5.0.3" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" @@ -1655,9 +1788,9 @@ "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==", + "version": "1.4.639", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.639.tgz", + "integrity": "sha512-CkKf3ZUVZchr+zDpAlNLEEy2NJJ9T64ULWaDgy3THXXlPVPkLu3VOs9Bac44nebVtdwl2geSj6AxTtGDOxoXhg==", "dev": true }, "node_modules/emoji-regex": { @@ -1695,9 +1828,9 @@ } }, "node_modules/engine.io": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", - "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -1707,29 +1840,21 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/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==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", "engines": { "node": ">=10.0.0" } }, - "node_modules/engine.io/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/engine.io/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1752,9 +1877,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "engines": { "node": ">=0.12" }, @@ -1798,12 +1923,15 @@ "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==", + "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": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/esprima": { @@ -1894,6 +2022,14 @@ "node": ">= 0.6" } }, + "node_modules/express/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "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", @@ -1941,16 +2077,19 @@ } }, "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==", + "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": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat": { @@ -1963,9 +2102,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -2059,6 +2198,20 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2068,22 +2221,22 @@ } }, "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", + "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" + "wide-align": "^1.1.5" }, "engines": { - "node": ">=10" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/gensync": { @@ -2114,13 +2267,14 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2136,14 +2290,14 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.1.1", + "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, @@ -2166,6 +2320,26 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2175,29 +2349,51 @@ "node": ">=4" } }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dependencies": { - "function-bind": "^1.1.1" + "get-intrinsic": "^1.1.3" }, - "engines": { - "node": ">= 0.4.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, "node_modules/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==", + "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": ">=4" + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-symbols": { @@ -2232,6 +2428,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2248,9 +2455,9 @@ "dev": true }, "node_modules/htmlparser2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", - "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -2260,9 +2467,9 @@ ], "dependencies": { "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", + "domhandler": "^5.0.3", "domutils": "^3.0.1", - "entities": "^4.3.0" + "entities": "^4.4.0" } }, "node_modules/http-cache-semantics": { @@ -2546,12 +2753,6 @@ "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", @@ -2620,18 +2821,6 @@ "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", @@ -2646,15 +2835,6 @@ "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", @@ -2774,13 +2954,12 @@ "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==", + "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": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -2862,9 +3041,9 @@ } }, "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==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", "dev": true }, "node_modules/jwa": { @@ -2887,15 +3066,18 @@ } }, "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==", + "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": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { @@ -2976,9 +3158,9 @@ } }, "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==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "engines": { "node": "14 || >=16.14" } @@ -2998,9 +3180,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -3096,14 +3278,15 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=10" } }, "node_modules/minipass": { @@ -3245,23 +3428,6 @@ "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", @@ -3285,198 +3451,24 @@ "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", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "engines": { "node": "*" } }, "node_modules/moment-timezone": { - "version": "0.5.43", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz", - "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==", + "version": "0.5.44", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.44.tgz", + "integrity": "sha512-nv3YpzI/8lkQn0U6RkLd+f0W/zy/JnoR5/EyPz/dNkPTBjA2jNLCVxaiQ8QpeLymhSZvX0wCL5s27NQWdOPwAw==", "dependencies": { "moment": "^2.29.4" }, @@ -3510,35 +3502,23 @@ } }, "node_modules/nise": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", - "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.7.tgz", + "integrity": "sha512-wWtNUhkT7k58uvWTB/Gy26eA/EJKtPZFVAhEilN5UYVmmGRYOURbejRUyKm0Uu9XVEW7K5nBOZfR8VMB4QR2RQ==", "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" + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" } }, "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" - } + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "dev": true }, "node_modules/node-addon-api": { "version": "4.3.0", @@ -3546,9 +3526,9 @@ "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" }, "node_modules/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==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -3588,38 +3568,6 @@ "node": ">= 10.12.0" } }, - "node_modules/node-gyp/node_modules/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, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "optional": true, - "dependencies": { - "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" - }, - "engines": { - "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", @@ -3632,40 +3580,10 @@ "node": ">=10" } }, - "node_modules/node-gyp/node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "optional": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/node-gyp/node_modules/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, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/node-gyp/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "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, "dependencies": { "lru-cache": "^6.0.0" @@ -3690,9 +3608,9 @@ } }, "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==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/node-tone": { @@ -3701,17 +3619,17 @@ "integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w==" }, "node_modules/nodemailer": { - "version": "6.9.2", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.2.tgz", - "integrity": "sha512-4+TYaa/e1nIxQfyw/WzNPYTEZ5OvHIDEnmjs4LPmIfccPQN+2CYKmGHjWixn/chzD3bmUTu5FMfpltizMxqzdg==", + "version": "6.9.8", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz", + "integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ==", "engines": { "node": ">=6.0.0" } }, "node_modules/nodemon": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", - "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", "dev": true, "dependencies": { "chokidar": "^3.5.2", @@ -3736,6 +3654,16 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/nodemon/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -3745,17 +3673,49 @@ "ms": "^2.1.1" } }, + "node_modules/nodemon/node_modules/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, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/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/nodemon/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/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "node_modules/nodemon/node_modules/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, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "dependencies": { "abbrev": "1" }, @@ -3763,7 +3723,7 @@ "nopt": "bin/nopt.js" }, "engines": { - "node": "*" + "node": ">=6" } }, "node_modules/normalize-path": { @@ -3776,14 +3736,18 @@ } }, "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "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, "dependencies": { - "are-we-there-yet": "^2.0.0", + "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", + "gauge": "^4.0.3", "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/nyc": { @@ -3827,27 +3791,124 @@ "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==", + "node_modules/nyc/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": { - "aggregate-error": "^3.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/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/nyc/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/nyc/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/nyc/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/nyc/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/nyc/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/nyc/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/nyc/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/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3865,9 +3926,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3908,11 +3969,11 @@ } }, "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==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.4.tgz", + "integrity": "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==", "dependencies": { - "jose": "^4.15.1", + "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -3948,39 +4009,12 @@ } }, "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==", + "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": "^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", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "optional": true, - "dependencies": { - "aggregate-error": "^3.0.0" + "p-limit": "^3.0.2" }, "engines": { "node": ">=10" @@ -3989,6 +4023,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -4101,9 +4147,9 @@ "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==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" }, "node_modules/picocolors": { "version": "1.0.0", @@ -4135,6 +4181,58 @@ "node": ">=8" } }, + "node_modules/pkg-dir/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/pkg-dir/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/pkg-dir/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/pkg-dir/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/process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -4351,14 +4449,14 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" }, "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -4393,9 +4491,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/sequelize": { - "version": "6.32.1", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.32.1.tgz", - "integrity": "sha512-3Iv0jruv57Y0YvcxQW7BE56O7DC1BojcfIrqh6my+IQwde+9u/YnuYHzK+8kmZLhLvaziRT1eWu38nh9yVwn/g==", + "version": "6.35.2", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.35.2.tgz", + "integrity": "sha512-EdzLaw2kK4/aOnWQ7ed/qh3B6/g+1DvmeXr66RwbcqSm/+QRS9X0LDI5INBibsy4eNJHWIRPo3+QK0zL+IPBHg==", "funding": [ { "type": "opencollective", @@ -4406,14 +4504,14 @@ "@types/debug": "^4.1.8", "@types/validator": "^13.7.17", "debug": "^4.3.4", - "dottie": "^2.0.4", + "dottie": "^2.0.6", "inflection": "^1.13.4", "lodash": "^4.17.21", "moment": "^2.29.4", "moment-timezone": "^0.5.43", - "pg-connection-string": "^2.6.0", + "pg-connection-string": "^2.6.1", "retry-as-promised": "^7.0.4", - "semver": "^7.5.1", + "semver": "^7.5.4", "sequelize-pool": "^7.1.0", "toposort-class": "^1.0.1", "uuid": "^8.3.2", @@ -4494,9 +4592,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/sequelize/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "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" }, @@ -4535,6 +4633,21 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -4618,15 +4731,6 @@ "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", @@ -4636,15 +4740,6 @@ "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", @@ -4668,30 +4763,34 @@ } }, "node_modules/socket.io": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz", - "integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", + "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", + "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.2.1", - "socket.io-adapter": "~2.4.0", - "socket.io-parser": "~4.2.1" + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/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==" + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dependencies": { + "ws": "~8.11.0" + } }, "node_modules/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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -4937,21 +5036,24 @@ } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "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": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/tar": { - "version": "6.1.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", - "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -4986,6 +5088,28 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/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/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -5032,6 +5156,21 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/touch/node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -5093,6 +5232,11 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -5171,9 +5315,9 @@ } }, "node_modules/validator": { - "version": "13.9.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", - "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", "engines": { "node": ">= 0.10" } @@ -5278,9 +5422,9 @@ } }, "node_modules/ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "engines": { "node": ">=10.0.0" }, @@ -5332,47 +5476,30 @@ "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==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "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" + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "engines": { - "node": ">=8" + "node": ">=10" } }, "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==", + "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": ">=0.10.0" + "node": ">=10" } }, "node_modules/yargs-unparser": { @@ -5402,21 +5529,18 @@ "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==", + "node_modules/yargs-unparser/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/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", @@ -5429,4062 +5553,5 @@ "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", - "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", - "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": { - "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", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "requires": { - "abbrev": "1" - } - }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "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": { - "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", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "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" - } - }, - "@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", - "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.7.17", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz", - "integrity": "sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==" - }, - "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.3.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", - "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", - "optional": true, - "requires": { - "debug": "^4.1.0", - "depd": "^2.0.0", - "humanize-ms": "^1.2.1" - }, - "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 - } - } - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "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", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "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", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "requires": { - "delegates": "^1.0.0", - "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", - "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==" - }, - "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" - } - }, - "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", - "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" - }, - "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" - } - } - } - }, - "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", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "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", - "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==", - "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", - "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" - } - }, - "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", - "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==" - }, - "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", - "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" - } - }, - "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", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "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", - "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.1", - "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", - "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==" - }, - "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", - "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 - }, - "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", - "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" - } - }, - "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", - "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==" - }, - "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", - "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.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "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" - } - }, - "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", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "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", - "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" - } - }, - "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", - "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==" - }, - "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", - "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==", - "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==", - "devOptional": 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 - }, - "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==", - "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 - }, - "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": { - "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.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", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "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", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "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" - }, - "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": { - "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==" - }, - "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", - "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==" - }, - "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", - "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" - } - }, - "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", - "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.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "optional": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "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", - "integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w==" - }, - "nodemailer": { - "version": "6.9.2", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.2.tgz", - "integrity": "sha512-4+TYaa/e1nIxQfyw/WzNPYTEZ5OvHIDEnmjs4LPmIfccPQN+2CYKmGHjWixn/chzD3bmUTu5FMfpltizMxqzdg==" - }, - "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" - } - }, - "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", - "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" - } - }, - "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", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "optional": true, - "requires": { - "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", - "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==" - }, - "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", - "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" - } - }, - "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", - "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" - } - }, - "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", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "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==" - }, - "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", - "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==" - }, - "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", - "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 - } - } - }, - "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", - "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 - } - } - }, - "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", - "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" - } - }, - "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", - "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" - } - }, - "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", - "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==" - } - } - }, - "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", - "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-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", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "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", - "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==" - }, - "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", - "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.9.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", - "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==" - }, - "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==", - "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", - "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": "*" - } - }, - "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", - "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==" - }, - "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 - } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index d7f38bcb..34865dd6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "start": "node index.js", "client": "cd client && npm ci && npm run generate", "prod": "npm run client && npm ci && node prod.js", - "build-win": "npm run client && pkg -t node16-win-x64 -o ./dist/win/audiobookshelf -C GZip .", + "build-win": "npm run client && pkg -t node18-win-x64 -o ./dist/win/audiobookshelf -C GZip .", "build-linux": "build/linuxpackager", "docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf", "docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local", @@ -48,7 +48,7 @@ "openid-client": "^5.6.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", - "sequelize": "^6.32.1", + "sequelize": "^6.35.2", "socket.io": "^4.5.4", "sqlite3": "^5.1.6", "ssrf-req-filter": "^1.1.0", diff --git a/readme.md b/readme.md index a3b84f00..99e24d47 100644 --- a/readme.md +++ b/readme.md @@ -309,7 +309,7 @@ You are now ready to start development! ### Manual Environment Setup -If you don't want to use the dev container, you can still develop this project. First, you will need to install [NodeJs](https://nodejs.org/) (version 16) and [FFmpeg](https://ffmpeg.org/). +If you don't want to use the dev container, you can still develop this project. First, you will need to install [NodeJs](https://nodejs.org/) (version 20) and [FFmpeg](https://ffmpeg.org/). Next you will need to create a `dev.js` file in the project's root directory. This contains configuration information and paths unique to your development environment. You can find an example of this file in `.devcontainer/dev.js`. From 71048c7ff091552decd224f5b9faeb571eb281a6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 20 Jan 2024 16:39:43 -0600 Subject: [PATCH 0315/2145] Remove support for Docker armv7 builds --- .github/workflows/docker-build.yml | 2 +- .github/workflows/integration-test.yml | 2 +- package.json | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 80439648..e783fce6 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -71,7 +71,7 @@ jobs: with: tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }} context: . - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64 push: true cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index f88db2ed..df639ef7 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -16,7 +16,7 @@ jobs: - name: setup nade uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20 - name: install pkg run: npm install -g pkg diff --git a/package.json b/package.json index 34865dd6..b44ef36e 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,9 @@ "prod": "npm run client && npm ci && node prod.js", "build-win": "npm run client && pkg -t node18-win-x64 -o ./dist/win/audiobookshelf -C GZip .", "build-linux": "build/linuxpackager", - "docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf", + "docker": "docker buildx build --platform linux/amd64,linux/arm64 --push . -t advplyr/audiobookshelf", "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", "test": "mocha", "coverage": "nyc mocha" From 19e18036334d5a44525f9b6fef745e5faf4c2f1b Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 22 Jan 2024 17:56:41 -0600 Subject: [PATCH 0316/2145] Remove unused import --- server/Auth.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/Auth.js b/server/Auth.js index d2334de2..88b25c66 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -8,7 +8,6 @@ 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. From 73c21242b4a70f0314bbfc62dd2841a38afc4bcb Mon Sep 17 00:00:00 2001 From: jfrazx <staringblind@gmail.com> Date: Mon, 22 Jan 2024 20:36:20 -0800 Subject: [PATCH 0317/2145] feat: utilize p-throttle instad of limiter --- package-lock.json | 26 +++++++++---------- package.json | 2 +- server/providers/Audnexus.js | 48 +++++++++++------------------------- 3 files changed, 28 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65316f65..30b40e28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,11 @@ "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", - "limiter": "^2.1.0", "lru-cache": "^10.0.3", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "openid-client": "^5.6.1", + "p-throttle": "^4.1.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", "sequelize": "^6.35.2", @@ -2841,11 +2841,6 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, - "node_modules/just-performance": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/just-performance/-/just-performance-4.3.0.tgz", - "integrity": "sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q==" - }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -2865,14 +2860,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/limiter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-2.1.0.tgz", - "integrity": "sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw==", - "dependencies": { - "just-performance": "4.3.0" - } - }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3977,6 +3964,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-4.1.1.tgz", + "integrity": "sha512-TuU8Ato+pRTPJoDzYD4s7ocJYcNSEZRvlxoq3hcPI2kZDZ49IQ1Wkj7/gDJc3X7XiEAAvRGtDzdXJI0tC3IL1g==", + "engines": { + "node": ">=10" + }, + "funding": { + "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", diff --git a/package.json b/package.json index 65dbdd1e..46624752 100644 --- a/package.json +++ b/package.json @@ -41,11 +41,11 @@ "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", - "limiter": "^2.1.0", "lru-cache": "^10.0.3", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "openid-client": "^5.6.1", + "p-throttle": "^4.1.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", "sequelize": "^6.35.2", diff --git a/server/providers/Audnexus.js b/server/providers/Audnexus.js index d9223374..14eab4a1 100644 --- a/server/providers/Audnexus.js +++ b/server/providers/Audnexus.js @@ -1,7 +1,7 @@ const axios = require('axios') const { levenshteinDistance } = require('../utils/index') const Logger = require('../Logger') -const { RateLimiter } = require('limiter') +const pThrottle = require('p-throttle') class Audnexus { static _instance = null @@ -14,11 +14,15 @@ class Audnexus { this.baseUrl = 'https://api.audnex.us' + // Rate limit is 100 requests per minute. // @see https://github.com/laxamentumtech/audnexus#-deployment- - this.limiter = new RateLimiter({ - tokensPerInterval: 100, - fireImmediately: true, - interval: 'minute', + this.limiter = pThrottle({ + // Setting the limit to 1 allows for a short pause between requests that is almost imperceptible to + // the end user. A larger limit will grab blocks faster and then wait for the alloted time(interval) before + // fetching another batch. + limit: 1, + strict: true, + interval: 300 }) Audnexus._instance = this @@ -31,8 +35,8 @@ class Audnexus { Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - return this._processRequest(() => axios.get(authorRequestUrl)) - .then((res) => res.data || []) + const throttle = this.limiter(() => axios.get(authorRequestUrl)) + return throttle().then((res) => res.data || []) .catch((error) => { Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error) return [] @@ -46,36 +50,14 @@ class Audnexus { Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - return this._processRequest(() => axios.get(authorRequestUrl)) - .then((res) => res.data) + const throttle = this.limiter(() => axios.get(authorRequestUrl)) + return throttle().then((res) => res.data) .catch((error) => { Logger.error(`[Audnexus] Author request failed for ${asin}`, error) return null }) } - /** - * @description Process a request with a rate limiter - * - * @param {*} request - * @returns - */ - async _processRequest(request) { - const remainingTokens = await this.limiter.removeTokens(1) - Logger.info(`[Audnexus] Attempting request with ${remainingTokens} remaining tokens and ${this.limiter.tokensThisInterval} this interval`) - - if (remainingTokens >= 1) { - return request() - } - - // 100 tokens(requests) per minute give a refresh of ~1.67 per second, - // so a 10 second wait will yield ~16.7 additional tokens - Logger.info('[Audnexus] Sleeping for 10 seconds') - await new Promise(resolve => setTimeout(resolve, 10000)) - - return this._processRequest(request) - } - async findAuthorByASIN(asin, region) { const author = await this.authorRequest(asin, region) @@ -111,8 +93,8 @@ class Audnexus { getChaptersByASIN(asin, region) { Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}/${region}`) - return this._processRequest(() => axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`)) - .then((res) => res.data) + const throttle = this.limiter(() => axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`)) + return throttle().then((res) => res.data) .catch((error) => { Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error) return null From 70827727aa8edfff13e8acfd8fe5c486b7f254bd Mon Sep 17 00:00:00 2001 From: jfrazx <staringblind@gmail.com> Date: Mon, 22 Jan 2024 22:19:05 -0800 Subject: [PATCH 0318/2145] feat(429): retry 429 request errors --- server/providers/Audnexus.js | 48 ++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/server/providers/Audnexus.js b/server/providers/Audnexus.js index 14eab4a1..9e4dc6b7 100644 --- a/server/providers/Audnexus.js +++ b/server/providers/Audnexus.js @@ -1,7 +1,7 @@ const axios = require('axios') const { levenshteinDistance } = require('../utils/index') const Logger = require('../Logger') -const pThrottle = require('p-throttle') +const Throttle = require('p-throttle') class Audnexus { static _instance = null @@ -16,13 +16,13 @@ class Audnexus { // Rate limit is 100 requests per minute. // @see https://github.com/laxamentumtech/audnexus#-deployment- - this.limiter = pThrottle({ - // Setting the limit to 1 allows for a short pause between requests that is almost imperceptible to - // the end user. A larger limit will grab blocks faster and then wait for the alloted time(interval) before - // fetching another batch. + this.limiter = Throttle({ + // Setting the limit to 1 allows for a short pause between requests that is imperceptible to the end user. + // A larger limit will grab blocks faster and then wait for the alloted time(interval) before + // fetching another batch, but with a discernable pause from the user perspective. limit: 1, strict: true, - interval: 300 + interval: 150 }) Audnexus._instance = this @@ -35,8 +35,8 @@ class Audnexus { Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - const throttle = this.limiter(() => axios.get(authorRequestUrl)) - return throttle().then((res) => res.data || []) + return this._processRequest(this.limiter(() => axios.get(authorRequestUrl))) + .then((res) => res.data || []) .catch((error) => { Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error) return [] @@ -50,8 +50,8 @@ class Audnexus { Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - const throttle = this.limiter(() => axios.get(authorRequestUrl)) - return throttle().then((res) => res.data) + return this._processRequest(this.limiter(() => axios.get(authorRequestUrl))) + .then((res) => res.data) .catch((error) => { Logger.error(`[Audnexus] Author request failed for ${asin}`, error) return null @@ -93,13 +93,35 @@ class Audnexus { getChaptersByASIN(asin, region) { Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}/${region}`) - const throttle = this.limiter(() => axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`)) - return throttle().then((res) => res.data) + return this._processRequest(this.limiter(() => axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`))) + .then((res) => res.data) .catch((error) => { Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error) return null }) } + + /** + * Internal method to process requests and retry if rate limit is exceeded. + */ + async _processRequest(request) { + try { + const response = await request() + return response + } catch (error) { + if (error.response?.status === 429) { + const retryAfter = parseInt(error.response.headers?.['retry-after'], 10) || 5 + + Logger.warn(`[Audnexus] Rate limit exceeded. Retrying in ${retryAfter} seconds.`) + await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)) + + return this._processRequest(request) + } + + throw error + } + } } -module.exports = Audnexus \ No newline at end of file +module.exports = Audnexus + From 399ba314a30fd6a4911f53d0eb83b8cf9251ec59 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 23 Jan 2024 15:48:58 -0600 Subject: [PATCH 0319/2145] Update github issue template to include Windows Tray App --- .github/ISSUE_TEMPLATE/bug.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index ca044b71..7f47d710 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -44,6 +44,7 @@ body: options: - Docker - Debian/PPA + - Windows Tray App - Built from source - Other validations: From 3906dca04e3fd71842334d530e74789a7aed6c06 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 23 Jan 2024 17:51:34 -0600 Subject: [PATCH 0320/2145] Update:RSS feeds only use chapter titles for episode titles if all audio tracks match chapter times #2543 --- server/objects/Feed.js | 36 +++++++++++++++++++++++++++++------ server/objects/FeedEpisode.js | 18 ++++++++++++++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/server/objects/Feed.js b/server/objects/Feed.js index 08a602ae..2fac5d91 100644 --- a/server/objects/Feed.js +++ b/server/objects/Feed.js @@ -84,6 +84,24 @@ class Feed { return episode.fullPath } + /** + * If chapters for an audiobook match the audio tracks then use chapter titles instead of audio file names + * + * @param {import('../objects/LibraryItem')} libraryItem + * @returns {boolean} + */ + checkUseChapterTitlesForEpisodes(libraryItem) { + const tracks = libraryItem.media.tracks + const chapters = libraryItem.media.chapters + if (tracks.length !== chapters.length) return false + for (let i = 0; i < tracks.length; i++) { + if (Math.abs(chapters[i].start - tracks[i].startOffset) >= 1) { + return false + } + } + return true + } + setFromItem(userId, slug, libraryItem, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) { const media = libraryItem.media const mediaMetadata = media.metadata @@ -128,9 +146,10 @@ class Feed { this.episodes.push(feedEpisode) }) } else { // AUDIOBOOK EPISODES + const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItem) media.tracks.forEach((audioTrack) => { const feedEpisode = new FeedEpisode() - feedEpisode.setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, this.meta) + feedEpisode.setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, this.meta, useChapterTitles) this.episodes.push(feedEpisode) }) } @@ -168,9 +187,10 @@ class Feed { this.episodes.push(feedEpisode) }) } else { // AUDIOBOOK EPISODES + const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItem) media.tracks.forEach((audioTrack) => { const feedEpisode = new FeedEpisode() - feedEpisode.setFromAudiobookTrack(libraryItem, this.serverAddress, this.slug, audioTrack, this.meta) + feedEpisode.setFromAudiobookTrack(libraryItem, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles) this.episodes.push(feedEpisode) }) } @@ -214,9 +234,10 @@ class Feed { itemsWithTracks.forEach((item, index) => { if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt + const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item) item.media.tracks.forEach((audioTrack) => { const feedEpisode = new FeedEpisode() - feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, index) + feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, useChapterTitles, index) this.episodes.push(feedEpisode) }) }) @@ -245,9 +266,10 @@ class Feed { itemsWithTracks.forEach((item, index) => { if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt + const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item) item.media.tracks.forEach((audioTrack) => { const feedEpisode = new FeedEpisode() - feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, index) + feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles, index) this.episodes.push(feedEpisode) }) }) @@ -295,9 +317,10 @@ class Feed { itemsWithTracks.forEach((item, index) => { if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt + const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item) item.media.tracks.forEach((audioTrack) => { const feedEpisode = new FeedEpisode() - feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, index) + feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, useChapterTitles, index) this.episodes.push(feedEpisode) }) }) @@ -329,9 +352,10 @@ class Feed { itemsWithTracks.forEach((item, index) => { if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt + const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item) item.media.tracks.forEach((audioTrack) => { const feedEpisode = new FeedEpisode() - feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, index) + feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles, index) this.episodes.push(feedEpisode) }) }) diff --git a/server/objects/FeedEpisode.js b/server/objects/FeedEpisode.js index b87cf88f..50e27cf6 100644 --- a/server/objects/FeedEpisode.js +++ b/server/objects/FeedEpisode.js @@ -97,7 +97,17 @@ class FeedEpisode { this.fullPath = episode.audioFile.metadata.path } - setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, additionalOffset = null) { + /** + * + * @param {import('../objects/LibraryItem')} libraryItem + * @param {string} serverAddress + * @param {string} slug + * @param {import('../objects/files/AudioTrack')} audioTrack + * @param {Object} meta + * @param {boolean} useChapterTitles + * @param {number} [additionalOffset] + */ + setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, useChapterTitles, additionalOffset = null) { // Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate> let timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order let episodeId = uuidv4() @@ -119,10 +129,10 @@ class FeedEpisode { if (libraryItem.media.tracks.length == 1) { // If audiobook is a single file, use book title instead of chapter/file title title = libraryItem.media.metadata.title } else { - if (libraryItem.media.chapters.length) { + if (useChapterTitles) { // If audio track start and chapter start are within 1 seconds of eachother then use the chapter title - var matchingChapter = libraryItem.media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1) - if (matchingChapter && matchingChapter.title) title = matchingChapter.title + const matchingChapter = libraryItem.media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1) + if (matchingChapter?.title) title = matchingChapter.title } } From 87ebf4722bd4a614289c9f7524ae4a86be999772 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Wed, 24 Jan 2024 22:47:50 +0100 Subject: [PATCH 0321/2145] OpenID/SSO: Implement Logout functionality --- client/pages/account.vue | 17 ++++-- server/Auth.js | 109 ++++++++++++++++++++++++++------------- 2 files changed, 87 insertions(+), 39 deletions(-) diff --git a/client/pages/account.vue b/client/pages/account.vue index 4bb68727..ba5370c3 100644 --- a/client/pages/account.vue +++ b/client/pages/account.vue @@ -86,15 +86,24 @@ export default { const logoutPayload = { socketId: rootSocket.id } - this.$axios.$post('/logout', logoutPayload).catch((error) => { - console.error(error) - }) + if (localStorage.getItem('token')) { localStorage.removeItem('token') } this.$store.commit('libraries/setUserPlaylists', []) this.$store.commit('libraries/setCollections', []) - this.$router.push('/login') + + this.$axios.$post('/logout').then((logoutPayload) => { + const redirect_url = logoutPayload.redirect_url + + if (redirect_url) { + window.location.href = redirect_url + } else { + this.$router.push('/login') + } + }).catch((error) => { + console.error(error) + }) }, resetForm() { this.password = null diff --git a/server/Auth.js b/server/Auth.js index 88b25c66..bcbb4c88 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -81,7 +81,8 @@ class Auth { authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL, token_endpoint: global.ServerSettings.authOpenIDTokenURL, userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL, - jwks_uri: global.ServerSettings.authOpenIDJwksURL + jwks_uri: global.ServerSettings.authOpenIDJwksURL, + end_session_endpoint: global.ServerSettings.authOpenIDLogoutURL }).Client const openIdClient = new openIdIssuerClient({ client_id: global.ServerSettings.authOpenIDClientID, @@ -153,6 +154,9 @@ class Auth { return } + // We also have to save the id_token for later (used for logout) because we cannot set cookies here + user.openid_id_token = tokenset.id_token + // permit login return done(null, user) })) @@ -183,49 +187,42 @@ class Auth { } /** - * Stores the client's choice how the login callback should happen in temp cookies + * Stores the client's choice of login callback method in temporary cookies. + * + * The `authMethod` parameter specifies the authentication strategy and can have the following values: + * - 'local': Standard authentication, + * - 'api': Authentication for API use + * - 'openid': OpenID authentication directly over web + * - 'openid-mobile': OpenID authentication, but done via an mobile device * * @param {import('express').Request} req * @param {import('express').Response} res + * @param {string} authMethod - The authentication method, default is 'local'. */ - paramsToCookies(req, res) { - // 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', 'true', { - 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 - }) + paramsToCookies(req, res, authMethod = 'local') { + const TWO_MINUTES = 120000 // 2 minutes in milliseconds + const isRest = ['api', 'openid-mobile'].includes(authMethod) + const callback = req.query.redirect_uri || req.query.callback - // persist state if passed in + // Set the 'is_rest' cookie based on the authentication method + res.cookie('is_rest', isRest.toString(), { maxAge: TWO_MINUTES, httpOnly: true }) + + // Additional handling for 'local' authMethod + if (!isRest) { + // Store 'auth_state' if present in the request if (req.query.state) { - res.cookie('auth_state', req.query.state, { - maxAge: 120000, // 2 min - httpOnly: true - }) + res.cookie('auth_state', req.query.state, { maxAge: TWO_MINUTES, 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 + // Validate and store the callback URL if (!callback) { - res.status(400).send({ - message: 'No callback parameter' - }) - return + return res.status(400).send({ message: 'No callback parameter' }) } - // store the callback url to the auth_cb cookie - res.cookie('auth_cb', callback, { - maxAge: 120000, // 2 min - httpOnly: true - }) + res.cookie('auth_cb', callback, { maxAge: TWO_MINUTES, httpOnly: true }) } + + // Store the authentication method for a year + res.cookie('auth_method', authMethod, { maxAge: 1000 * 60 * 60 * 24 * 365, httpOnly: true }) } /** @@ -364,7 +361,7 @@ class Auth { }) // params (isRest, callback) to a cookie that will be send to the client - this.paramsToCookies(req, res) + this.paramsToCookies(req, res, mobile_redirect_uri ? 'openid-mobile' : 'openid') // Redirect the user agent (browser) to the authorization URL res.redirect(authorizationUrl) @@ -453,6 +450,12 @@ class Auth { if (loginError) { return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`) } + + // The id_token does not provide access to the user, but is used to identify the user to the SSO provider + // instead it containts a JWT with userinfo like user email, username, etc. + // the client will get to know it anyway in the logout url according to the oauth2 spec + // so it is safe to send it to the client, but we use strict settings + res.cookie('openid_id_token', user.openid_id_token, { maxAge: 1000 * 60 * 60 * 24 * 365, httpOnly: true, secure: true, sameSite: 'Strict' }) next() }) } @@ -521,7 +524,43 @@ class Auth { if (err) { res.sendStatus(500) } else { - res.sendStatus(200) + const authMethod = req.cookies.auth_method + + res.clearCookie('auth_method') + + if (authMethod === 'openid' || authMethod === 'openid-mobile') { + // If we are using openid, we need to redirect to the logout endpoint + // node-openid-client does not support doing it over passport + const oidcStrategy = passport._strategy('openid-client') + const client = oidcStrategy._client + + let postLogoutRedirectUri = null + + if (authMethod === 'openid') { + const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http' + const host = req.get('host') + // TODO: ABS does currently not support subfolders for installation + // If we want to support it we need to include a config for the serverurl + postLogoutRedirectUri = `${protocol}://${host}/login` + } + // else for openid-mobile we keep postLogoutRedirectUri on null + // nice would be to redirect to the app, but for example Authentik does not implement + // the post_logout_redirect_uri parameter at all and for other providers + // we would also need again to implement (and even before get to know somehow for 3rd party apps) + // the correct app link like audiobookshelf://login (and maybe also provide a redirect like mobile-redirect) + + const logoutUrl = client.endSessionUrl({ + id_token_hint: req.cookies.openid_id_token, + post_logout_redirect_uri: postLogoutRedirectUri + }) + + res.clearCookie('openid_id_token') + + // Tell the user agent (browser) to redirect to the authentification provider's logout URL + res.send({ redirect_url: logoutUrl }) + } else { + res.sendStatus(200) + } } }) }) From 0153c0faaee979a1790e910511f4671044d26a0d Mon Sep 17 00:00:00 2001 From: advplyr <dev@advplyr.com> Date: Wed, 24 Jan 2024 16:54:54 -0600 Subject: [PATCH 0322/2145] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 86fa46f0..19507e9e 100644 --- a/readme.md +++ b/readme.md @@ -243,7 +243,7 @@ subdomain.domain.com { ``` ### HAproxy -Bellow is a geneic HAproxy config, using `audiobookshelf.YOUR_DOMAIN.COM`. +Below is a generic HAProxy config, using `audiobookshelf.YOUR_DOMAIN.COM`. To use `http2`, `ssl` is needed. From e682213681bda3921a17b4ae54c174bc0771c129 Mon Sep 17 00:00:00 2001 From: advplyr <dev@advplyr.com> Date: Wed, 24 Jan 2024 16:55:06 -0600 Subject: [PATCH 0323/2145] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 19507e9e..83482b23 100644 --- a/readme.md +++ b/readme.md @@ -241,7 +241,7 @@ subdomain.domain.com { reverse_proxy <LOCAL_IP>:<PORT> } ``` -### HAproxy +### HAProxy Below is a generic HAProxy config, using `audiobookshelf.YOUR_DOMAIN.COM`. From bbc1d20396a3d02b2edd498fa97492019ed72ad4 Mon Sep 17 00:00:00 2001 From: advplyr <dev@advplyr.com> Date: Wed, 24 Jan 2024 16:55:12 -0600 Subject: [PATCH 0324/2145] Update readme.md --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 83482b23..1af28065 100644 --- a/readme.md +++ b/readme.md @@ -284,9 +284,9 @@ backend default_backend ```` -### PFsense and HAproxy +### pfSense and HAProxy -For PFsense the inputs are graphical, and `Health checking` is enabled. +For pfSense the inputs are graphical, and `Health checking` is enabled. #### Frontend, Default backend, access control lists and actions From dcec2154c01e0094c1bba5e61619d2a37aa32a8a Mon Sep 17 00:00:00 2001 From: advplyr <dev@advplyr.com> Date: Wed, 24 Jan 2024 16:55:17 -0600 Subject: [PATCH 0325/2145] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 1af28065..3ee435dc 100644 --- a/readme.md +++ b/readme.md @@ -324,7 +324,7 @@ The `Name` needs to match the `Parameters` above `audiobookshelf`. Health checking is enabled by default. `Http check method` of `OPTIONS` is not supported on Audiobookshelf. If Health check fails, data will not be forwared. -Need to one of following: +Need to do one of following: * Change `Health check method` to `none`. To disable. * Change `Http check method` to `HEAD` or `GET`. To make Health checking function. From 02aabb8f977a4bfd954d1c1b4a4e4798236bbf47 Mon Sep 17 00:00:00 2001 From: advplyr <dev@advplyr.com> Date: Wed, 24 Jan 2024 16:55:21 -0600 Subject: [PATCH 0326/2145] Update readme.md --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 3ee435dc..0c3651dd 100644 --- a/readme.md +++ b/readme.md @@ -326,8 +326,8 @@ Health checking is enabled by default. `Http check method` of `OPTIONS` is not s If Health check fails, data will not be forwared. Need to do one of following: -* Change `Health check method` to `none`. To disable. -* Change `Http check method` to `HEAD` or `GET`. To make Health checking function. +* To disable: Change `Health check method` to `none`. +* To make Health checking function: Change `Http check method` to `HEAD` or `GET`. # Run from source From f12ac685e8d9cd2284bdb5e08d7abdc1e2552041 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Thu, 25 Jan 2024 11:13:34 +0100 Subject: [PATCH 0327/2145] /auth/openid: Restructure - Distingush more explictly between mobile and web flow and simplify logic - Allow state parameter to be passed in mobile flow - Additional checks for correct parameters - Remove unused id_token code - Enforce S256 and don't allow plain PKCE --- server/Auth.js | 154 +++++++++++++++++++++++++++---------------------- 1 file changed, 84 insertions(+), 70 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index bcbb4c88..4a06cb12 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -266,92 +266,69 @@ class Auth { // openid strategy login route (this redirects to the configured openid login provider) router.get('/auth/openid', (req, res, next) => { + // 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 client = oidcStrategy._client + const sessionKey = oidcStrategy._key + 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 + const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http' + const hostUrl = new URL(`${protocol}://${req.get('host')}`) + const isMobileFlow = req.query.response_type === 'code' || req.query.redirect_uri || req.query.code_challenge + + // Only allow code flow (for mobile clients) + if (req.query.response_type && req.query.response_type !== 'code') { + Logger.debug(`[Auth] OIDC Invalid response_type=${req.query.response_type}`) + return res.status(400).send('Invalid response_type, only code supported') } - // 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' + // Generate a state on web flow or if no state supplied + const state = (!isMobileFlow || !req.query.state) ? OpenIDClient.generators.random() : req.query.state - 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 - // 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() - mobile_redirect_uri = req.query.redirect_uri - } else { - Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri} - not in whitelist`) + // Redirect URL for the SSO provider + let redirectUri + if (isMobileFlow) { + // Mobile required redirect uri + // If it is in the whitelist, we will save into this.openIdAuthSession and set the redirect uri to /auth/openid/mobile-redirect + // where we will handle the redirect to it + if (!req.query.redirect_uri || !isValidRedirectUri(req.query.redirect_uri)) { + Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri}`) return res.status(400).send('Invalid redirect_uri') } + // We cannot save the supplied 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(state, { mobile_redirect_uri: req.query.redirect_uri }) + + redirectUri = new URL('/auth/openid/mobile-redirect', hostUrl).toString() } else { - oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString() - } + redirectUri = new URL('/auth/openid/callback', hostUrl).toString() - Logger.debug(`[Auth] 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') + if (req.query.state) { + Logger.debug(`[Auth] Invalid state - not allowed on web openid flow`) + return res.status(400).send('Invalid state, not allowed on web flow') } - } 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 } } + oidcStrategy._params.redirect_uri = redirectUri + Logger.debug(`[Auth] OIDC redirect_uri=${redirectUri}`) + + let { code_challenge, code_challenge_method, code_verifier } = generatePkce(req, isMobileFlow) const params = { - state: OpenIDClient.generators.random(), - // Other params by the passport strategy + state, + // other passport strategy params and redirect_uri ...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'), + ...pick(params, 'state', 'max_age', 'response_type'), + code_verifier: code_verifier, // not null if web flow 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, { mobile_redirect_uri: mobile_redirect_uri }) - - // Now get the URL to direct to const authorizationUrl = client.authorizationUrl({ ...params, scope: 'openid profile email', @@ -360,15 +337,49 @@ class Auth { code_challenge_method }) - // params (isRest, callback) to a cookie that will be send to the client - this.paramsToCookies(req, res, mobile_redirect_uri ? 'openid-mobile' : 'openid') + this.paramsToCookies(req, res, isMobileFlow ? 'openid-mobile' : 'openid') - // 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') } + + function generatePkce(req, isMobileFlow) { + if (isMobileFlow) { + if (!req.query.code_challenge) { + throw new Error('code_challenge required for mobile flow (PKCE)') + } + if (req.query.code_challenge_method && req.query.code_challenge_method !== 'S256') { + throw new Error('Only S256 code_challenge_method method supported') + } + return { + code_challenge: req.query.code_challenge, + code_challenge_method: req.query.code_challenge_method || 'S256' + } + } else { + const code_verifier = OpenIDClient.generators.codeVerifier() + const code_challenge = OpenIDClient.generators.codeChallenge(code_verifier) + return { code_challenge, code_challenge_method: 'S256', code_verifier } + } + } + + function isValidRedirectUri(uri) { + // Check if the redirect_uri is in the whitelist + return Database.serverSettings.authOpenIDMobileRedirectURIs.includes(uri) || + (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*') + } + + // 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 + } }) // This will be the oauth2 callback route for mobile clients @@ -544,10 +555,13 @@ class Auth { postLogoutRedirectUri = `${protocol}://${host}/login` } // else for openid-mobile we keep postLogoutRedirectUri on null - // nice would be to redirect to the app, but for example Authentik does not implement + // nice would be to redirect to the app here, but for example Authentik does not implement // the post_logout_redirect_uri parameter at all and for other providers // we would also need again to implement (and even before get to know somehow for 3rd party apps) - // the correct app link like audiobookshelf://login (and maybe also provide a redirect like mobile-redirect) + // the correct app link like audiobookshelf://login (and maybe also provide a redirect like mobile-redirect). + // Instead because its null (and this way the parameter will be omitted completly), the client/app can simply append something like + // &post_logout_redirect_uri=audiobookshelf://login to the received logout url by itself which is the simplest solution + // (The URL needs to be whitelisted in the config of the SSO/ID provider) const logoutUrl = client.endSessionUrl({ id_token_hint: req.cookies.openid_id_token, From d4ed6348ee598209698e435fe1125a27f13c216d Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Thu, 25 Jan 2024 11:20:44 +0100 Subject: [PATCH 0328/2145] Auth: Store auth_method longer Its not unrealistic that someone keeps being logged into the app for more than a year if not stored longer logout process might not work anymore --- server/Auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 4a06cb12..c19f4a77 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -221,8 +221,8 @@ class Auth { res.cookie('auth_cb', callback, { maxAge: TWO_MINUTES, httpOnly: true }) } - // Store the authentication method for a year - res.cookie('auth_method', authMethod, { maxAge: 1000 * 60 * 60 * 24 * 365, httpOnly: true }) + // Store the authentication method for long + res.cookie('auth_method', authMethod, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true }) } /** From edb5ff1e33c3bf85c2336ea91af0bc76eb3f3a49 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Thu, 25 Jan 2024 11:44:20 +0100 Subject: [PATCH 0329/2145] SSO: Remove pick function --- server/Auth.js | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index c19f4a77..a330036c 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -315,23 +315,19 @@ class Auth { let { code_challenge, code_challenge_method, code_verifier } = generatePkce(req, isMobileFlow) - const params = { - state, - // other passport strategy params and redirect_uri - ...oidcStrategy._params - } - req.session[sessionKey] = { ...req.session[sessionKey], - ...pick(params, 'state', 'max_age', 'response_type'), + state: state, + max_age: oidcStrategy._params.max_age, + response_type: 'code', code_verifier: code_verifier, // not null if web flow 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 } const authorizationUrl = client.authorizationUrl({ - ...params, - scope: 'openid profile email', + ...oidcStrategy._params, + state: state, response_type: 'code', code_challenge, code_challenge_method @@ -369,17 +365,6 @@ class Auth { return Database.serverSettings.authOpenIDMobileRedirectURIs.includes(uri) || (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*') } - - // 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 - } }) // This will be the oauth2 callback route for mobile clients From 71b0a5cc818154375a22b5804b2ae49d138cc454 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Thu, 25 Jan 2024 11:49:10 +0100 Subject: [PATCH 0330/2145] SSO Settings: Fix Redirect URL Regex Forgot to include subpaths --- client/pages/config/authentication.vue | 2 +- server/controllers/MiscController.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 9e028307..67de9c35 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -193,7 +193,7 @@ export default { function isValidRedirectURI(uri) { // Check for somestring://someother/string - const pattern = new RegExp('^\\w+://[\\w\\.-]+$', 'i') + const pattern = new RegExp('^\\w+://[\\w\\.-]+(/[\\w\\./-]*)*$', 'i') return pattern.test(uri) } diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index c2272ee6..c098514e 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -633,7 +633,7 @@ class MiscController { } else if (key === 'authOpenIDMobileRedirectURIs') { function isValidRedirectURI(uri) { if (typeof uri !== 'string') return false - const pattern = new RegExp('^\\w+://[\\w.-]+$', 'i') + const pattern = new RegExp('^\\w+://[\\w\\.-]+(/[\\w\\./-]*)*$', 'i') return pattern.test(uri) } From 82048cd4f3caf8cbb2a15cada47009b072f809f6 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Thu, 25 Jan 2024 15:13:56 +0100 Subject: [PATCH 0331/2145] SSO: Also save openid_id_token longer --- server/Auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Auth.js b/server/Auth.js index a330036c..a2f6cfd2 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -451,7 +451,7 @@ class Auth { // instead it containts a JWT with userinfo like user email, username, etc. // the client will get to know it anyway in the logout url according to the oauth2 spec // so it is safe to send it to the client, but we use strict settings - res.cookie('openid_id_token', user.openid_id_token, { maxAge: 1000 * 60 * 60 * 24 * 365, httpOnly: true, secure: true, sameSite: 'Strict' }) + res.cookie('openid_id_token', user.openid_id_token, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true, secure: true, sameSite: 'Strict' }) next() }) } From c3ba7daa16aced1761a6850ec565b3c8df0d0364 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Thu, 25 Jan 2024 16:05:41 +0100 Subject: [PATCH 0332/2145] Auth: Remove is_rest cookie --- server/Auth.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index a2f6cfd2..9dfe2416 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -186,6 +186,16 @@ class Auth { } } + /** + * Returns if the given auth method is API based. + * + * @param {string} authMethod + * @returns {boolean} + */ + isAuthMethodAPIBased(authMethod) { + return ['api', 'openid-mobile'].includes(authMethod) + } + /** * Stores the client's choice of login callback method in temporary cookies. * @@ -201,14 +211,10 @@ class Auth { */ paramsToCookies(req, res, authMethod = 'local') { const TWO_MINUTES = 120000 // 2 minutes in milliseconds - const isRest = ['api', 'openid-mobile'].includes(authMethod) const callback = req.query.redirect_uri || req.query.callback - // Set the 'is_rest' cookie based on the authentication method - res.cookie('is_rest', isRest.toString(), { maxAge: TWO_MINUTES, httpOnly: true }) - - // Additional handling for 'local' authMethod - if (!isRest) { + // Additional handling for non-API based authMethod + if (!this.isAuthMethodAPIBased(authMethod)) { // Store 'auth_state' if present in the request if (req.query.state) { res.cookie('auth_state', req.query.state, { maxAge: TWO_MINUTES, httpOnly: true }) @@ -236,7 +242,7 @@ 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 === 'true') { + if (this.isAuthMethodAPIBased(req.cookies.auth_method)) { // REST request - send data res.json(data_json) } else { From d43a1109c82bad451d9e4df9f4eef103e3ed60b2 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 25 Jan 2024 17:51:06 +0200 Subject: [PATCH 0333/2145] Modify BinaryManager to download version 6.1 and remove old dowloaded versions --- server/managers/BinaryManager.js | 51 ++++- server/utils/fileUtils.js | 3 +- test/server/managers/BinaryManager.test.js | 207 ++++++++++++++++----- 3 files changed, 207 insertions(+), 54 deletions(-) diff --git a/server/managers/BinaryManager.js b/server/managers/BinaryManager.js index e9aab609..1a898236 100644 --- a/server/managers/BinaryManager.js +++ b/server/managers/BinaryManager.js @@ -1,3 +1,6 @@ +const child_process = require('child_process') +const { promisify } = require('util') +const exec = promisify(child_process.exec) const path = require('path') const which = require('../libs/which') const fs = require('../libs/fsExtra') @@ -12,16 +15,20 @@ class BinaryManager { { name: 'ffprobe', envVariable: 'FFPROBE_PATH' } ] + goodVersions = [ '5.1', '6' ] + constructor(requiredBinaries = this.defaultRequiredBinaries) { this.requiredBinaries = requiredBinaries this.mainInstallPath = process.pkg ? path.dirname(process.execPath) : global.appRoot this.altInstallPath = global.ConfigPath + this.exec = exec } async init() { if (this.initialized) return const missingBinaries = await this.findRequiredBinaries() if (missingBinaries.length == 0) return + await this.removeOldBinaries(missingBinaries) await this.install(missingBinaries) const missingBinariesAfterInstall = await this.findRequiredBinaries() if (missingBinariesAfterInstall.length != 0) { @@ -31,18 +38,29 @@ class BinaryManager { this.initialized = true } + async removeOldBinaries(binaryNames) { + for (const binaryName of binaryNames) { + const executable = this.getExecutableFileName(binaryName) + const mainInstallPath = path.join(this.mainInstallPath, executable) + const altInstallPath = path.join(this.altInstallPath, executable) + Logger.debug(`[BinaryManager] Removing old binaries: ${mainInstallPath}, ${altInstallPath}`) + await fs.remove(mainInstallPath) + await fs.remove(altInstallPath) + } + } + async findRequiredBinaries() { const missingBinaries = [] for (const binary of this.requiredBinaries) { const binaryPath = await this.findBinary(binary.name, binary.envVariable) if (binaryPath) { - Logger.info(`[BinaryManager] Found ${binary.name} at ${binaryPath}`) + Logger.info(`[BinaryManager] Found good ${binary.name} at ${binaryPath}`) if (process.env[binary.envVariable] !== binaryPath) { Logger.info(`[BinaryManager] Updating process.env.${binary.envVariable}`) process.env[binary.envVariable] = binaryPath } } else { - Logger.info(`[BinaryManager] ${binary.name} not found`) + Logger.info(`[BinaryManager] ${binary.name} not found or version too old`) missingBinaries.push(binary.name) } } @@ -50,25 +68,42 @@ class BinaryManager { } async findBinary(name, envVariable) { - const executable = name + (process.platform == 'win32' ? '.exe' : '') + const executable = this.getExecutableFileName(name) const defaultPath = process.env[envVariable] - if (defaultPath && await fs.pathExists(defaultPath)) return defaultPath + if (await this.isBinaryGood(defaultPath)) return defaultPath const whichPath = which.sync(executable, { nothrow: true }) - if (whichPath) return whichPath + if (await this.isBinaryGood(whichPath)) return whichPath const mainInstallPath = path.join(this.mainInstallPath, executable) - if (await fs.pathExists(mainInstallPath)) return mainInstallPath + if (await this.isBinaryGood(mainInstallPath)) return mainInstallPath const altInstallPath = path.join(this.altInstallPath, executable) - if (await fs.pathExists(altInstallPath)) return altInstallPath + if (await this.isBinaryGood(altInstallPath)) return altInstallPath return null } + async isBinaryGood(binaryPath) { + if (!binaryPath || !await fs.pathExists(binaryPath)) return false + try { + const { stdout } = await this.exec('"' + binaryPath + '"' + ' -version') + const version = stdout.match(/version\s([\d\.]+)/)?.[1] + if (!version) return false + return this.goodVersions.some(goodVersion => version.startsWith(goodVersion)) + } catch (err) { + Logger.error(`[BinaryManager] Failed to check version of ${binaryPath}`) + return false + } + } + async install(binaries) { if (binaries.length == 0) return Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`) let destination = await fileUtils.isWritable(this.mainInstallPath) ? this.mainInstallPath : this.altInstallPath - await ffbinaries.downloadBinaries(binaries, { destination }) + await ffbinaries.downloadBinaries(binaries, { destination, version: '6.1', force: true }) Logger.info(`[BinaryManager] Binaries installed to ${destination}`) } + + getExecutableFileName(name) { + return name + (process.platform == 'win32' ? '.exe' : '') + } } module.exports = BinaryManager \ No newline at end of file diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 14e4d743..b01a186c 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -370,11 +370,12 @@ module.exports.encodeUriPath = (path) => { */ module.exports.isWritable = async (directory) => { try { - const accessTestFile = path.join(directory, 'accessTest') + const accessTestFile = Path.join(directory, 'accessTest') await fs.writeFile(accessTestFile, '') await fs.remove(accessTestFile) return true } catch (err) { + Logger.info(`[fileUtils] Directory is not writable "${directory}"`, err) return false } } diff --git a/test/server/managers/BinaryManager.test.js b/test/server/managers/BinaryManager.test.js index b93973e0..48aa5e3f 100644 --- a/test/server/managers/BinaryManager.test.js +++ b/test/server/managers/BinaryManager.test.js @@ -15,6 +15,7 @@ describe('BinaryManager', () => { describe('init', () => { let findStub let installStub + let removeOldBinariesStub let errorStub let exitStub @@ -22,6 +23,7 @@ describe('BinaryManager', () => { binaryManager = new BinaryManager() findStub = sinon.stub(binaryManager, 'findRequiredBinaries') installStub = sinon.stub(binaryManager, 'install') + removeOldBinariesStub = sinon.stub(binaryManager, 'removeOldBinaries') errorStub = sinon.stub(console, 'error') exitStub = sinon.stub(process, 'exit') }) @@ -29,6 +31,7 @@ describe('BinaryManager', () => { afterEach(() => { findStub.restore() installStub.restore() + removeOldBinariesStub.restore() errorStub.restore() exitStub.restore() }) @@ -39,6 +42,7 @@ describe('BinaryManager', () => { await binaryManager.init() expect(installStub.called).to.be.false + expect(removeOldBinariesStub.called).to.be.false expect(findStub.calledOnce).to.be.true expect(errorStub.called).to.be.false expect(exitStub.called).to.be.false @@ -54,6 +58,7 @@ describe('BinaryManager', () => { expect(findStub.calledTwice).to.be.true expect(installStub.calledOnce).to.be.true + expect(removeOldBinariesStub.calledOnce).to.be.true expect(errorStub.called).to.be.false expect(exitStub.called).to.be.false }) @@ -68,6 +73,7 @@ describe('BinaryManager', () => { expect(findStub.calledTwice).to.be.true expect(installStub.calledOnce).to.be.true + expect(removeOldBinariesStub.calledOnce).to.be.true expect(errorStub.calledOnce).to.be.true expect(exitStub.calledOnce).to.be.true expect(exitStub.calledWith(1)).to.be.true @@ -171,7 +177,7 @@ describe('BinaryManager', () => { describe('findBinary', () => { let binaryManager - let fsPathExistsStub + let isBinaryGoodStub let whichSyncStub let mainInstallPath let altInstallPath @@ -185,7 +191,7 @@ describe('findBinary', () => { beforeEach(() => { binaryManager = new BinaryManager() - fsPathExistsStub = sinon.stub(fs, 'pathExists') + isBinaryGoodStub = sinon.stub(binaryManager, 'isBinaryGood') whichSyncStub = sinon.stub(which, 'sync') binaryManager.mainInstallPath = '/path/to/main/install' mainInstallPath = path.join(binaryManager.mainInstallPath, executable) @@ -194,71 +200,182 @@ describe('findBinary', () => { }) afterEach(() => { - fsPathExistsStub.restore() + isBinaryGoodStub.restore() whichSyncStub.restore() }) - - it('should return defaultPath if it exists', async () => { + + it('should return the defaultPath if it exists and is a good binary', async () => { process.env[envVariable] = defaultPath - fsPathExistsStub.withArgs(defaultPath).resolves(true) - + isBinaryGoodStub.withArgs(defaultPath).resolves(true) + const result = await binaryManager.findBinary(name, envVariable) - - expect(result).to.equal(defaultPath) - expect(fsPathExistsStub.calledOnceWith(defaultPath)).to.be.true - expect(whichSyncStub.notCalled).to.be.true + + expect(result).to.equal(defaultPath) + expect(isBinaryGoodStub.calledOnce).to.be.true + expect(isBinaryGoodStub.calledWith(defaultPath)).to.be.true }) - - it('should return whichPath if it exists', async () => { + + it('should return the whichPath if it exists and is a good binary', async () => { delete process.env[envVariable] + isBinaryGoodStub.withArgs(undefined).resolves(false) + isBinaryGoodStub.withArgs(whichPath).resolves(true) whichSyncStub.returns(whichPath) - + const result = await binaryManager.findBinary(name, envVariable) - + expect(result).to.equal(whichPath) - expect(fsPathExistsStub.notCalled).to.be.true - expect(whichSyncStub.calledOnce).to.be.true + expect(isBinaryGoodStub.calledTwice).to.be.true + expect(isBinaryGoodStub.calledWith(undefined)).to.be.true + expect(isBinaryGoodStub.calledWith(whichPath)).to.be.true }) - - it('should return mainInstallPath if it exists', async () => { + + it('should return the mainInstallPath if it exists and is a good binary', async () => { delete process.env[envVariable] + isBinaryGoodStub.withArgs(undefined).resolves(false) + isBinaryGoodStub.withArgs(null).resolves(false) + isBinaryGoodStub.withArgs(mainInstallPath).resolves(true) whichSyncStub.returns(null) - fsPathExistsStub.withArgs(mainInstallPath).resolves(true) - + const result = await binaryManager.findBinary(name, envVariable) - + expect(result).to.equal(mainInstallPath) - expect(whichSyncStub.calledOnce).to.be.true - expect(fsPathExistsStub.calledOnceWith(mainInstallPath)).to.be.true + expect(isBinaryGoodStub.callCount).to.be.equal(3) + expect(isBinaryGoodStub.calledWith(undefined)).to.be.true + expect(isBinaryGoodStub.calledWith(null)).to.be.true + expect(isBinaryGoodStub.calledWith(mainInstallPath)).to.be.true }) - - it('should return altInstallPath if it exists', async () => { + + it('should return the altInstallPath if it exists and is a good binary', async () => { delete process.env[envVariable] + isBinaryGoodStub.withArgs(undefined).resolves(false) + isBinaryGoodStub.withArgs(null).resolves(false) + isBinaryGoodStub.withArgs(mainInstallPath).resolves(false) + isBinaryGoodStub.withArgs(altInstallPath).resolves(true) whichSyncStub.returns(null) - fsPathExistsStub.withArgs(mainInstallPath).resolves(false) - fsPathExistsStub.withArgs(altInstallPath).resolves(true) - + const result = await binaryManager.findBinary(name, envVariable) - + expect(result).to.equal(altInstallPath) - expect(whichSyncStub.calledOnce).to.be.true - expect(fsPathExistsStub.calledTwice).to.be.true - expect(fsPathExistsStub.calledWith(mainInstallPath)).to.be.true - expect(fsPathExistsStub.calledWith(altInstallPath)).to.be.true + expect(isBinaryGoodStub.callCount).to.be.equal(4) + expect(isBinaryGoodStub.calledWith(undefined)).to.be.true + expect(isBinaryGoodStub.calledWith(null)).to.be.true + expect(isBinaryGoodStub.calledWith(mainInstallPath)).to.be.true + expect(isBinaryGoodStub.calledWith(altInstallPath)).to.be.true + }) + + it('should return null if no good binary is found', async () => { + delete process.env[envVariable] + isBinaryGoodStub.withArgs(undefined).resolves(false) + isBinaryGoodStub.withArgs(null).resolves(false) + isBinaryGoodStub.withArgs(mainInstallPath).resolves(false) + isBinaryGoodStub.withArgs(altInstallPath).resolves(false) + whichSyncStub.returns(null) + + const result = await binaryManager.findBinary(name, envVariable) + + expect(result).to.be.null + expect(isBinaryGoodStub.callCount).to.be.equal(4) + expect(isBinaryGoodStub.calledWith(undefined)).to.be.true + expect(isBinaryGoodStub.calledWith(null)).to.be.true + expect(isBinaryGoodStub.calledWith(mainInstallPath)).to.be.true + expect(isBinaryGoodStub.calledWith(altInstallPath)).to.be.true + }) +}) + +describe('isBinaryGood', () => { + let binaryManager + let fsPathExistsStub + let execStub + let loggerInfoStub + let loggerErrorStub + + const binaryPath = '/path/to/binary' + const execCommand = '"' + binaryPath + '"' + ' -version' + + beforeEach(() => { + binaryManager = new BinaryManager() + fsPathExistsStub = sinon.stub(fs, 'pathExists') + execStub = sinon.stub(binaryManager, 'exec') }) - it('should return null if binary is not found', async () => { - delete process.env[envVariable] - whichSyncStub.returns(null) - fsPathExistsStub.withArgs(mainInstallPath).resolves(false) - fsPathExistsStub.withArgs(altInstallPath).resolves(false) + afterEach(() => { + fsPathExistsStub.restore() + execStub.restore() + }) - const result = await binaryManager.findBinary(name, envVariable) + it('should return false if binaryPath is falsy', async () => { + fsPathExistsStub.resolves(true) - expect(result).to.be.null - expect(whichSyncStub.calledOnce).to.be.true - expect(fsPathExistsStub.calledTwice).to.be.true - expect(fsPathExistsStub.calledWith(mainInstallPath)).to.be.true - expect(fsPathExistsStub.calledWith(altInstallPath)).to.be.true + const result = await binaryManager.isBinaryGood(null) + + expect(result).to.be.false + expect(fsPathExistsStub.called).to.be.false + expect(execStub.called).to.be.false + }) + + it('should return false if binaryPath does not exist', async () => { + fsPathExistsStub.resolves(false) + + const result = await binaryManager.isBinaryGood(binaryPath) + + expect(result).to.be.false + expect(fsPathExistsStub.calledOnce).to.be.true + expect(fsPathExistsStub.calledWith(binaryPath)).to.be.true + expect(execStub.called).to.be.false + }) + + it('should return false if failed to check version of binary', async () => { + fsPathExistsStub.resolves(true) + execStub.rejects(new Error('Failed to execute command')) + + const result = await binaryManager.isBinaryGood(binaryPath) + + expect(result).to.be.false + expect(fsPathExistsStub.calledOnce).to.be.true + expect(fsPathExistsStub.calledWith(binaryPath)).to.be.true + expect(execStub.calledOnce).to.be.true + expect(execStub.calledWith(execCommand)).to.be.true + }) + + it('should return false if version is not found', async () => { + const stdout = 'Some output without version' + fsPathExistsStub.resolves(true) + execStub.resolves({ stdout }) + + const result = await binaryManager.isBinaryGood(binaryPath) + + expect(result).to.be.false + expect(fsPathExistsStub.calledOnce).to.be.true + expect(fsPathExistsStub.calledWith(binaryPath)).to.be.true + expect(execStub.calledOnce).to.be.true + expect(execStub.calledWith(execCommand)).to.be.true + }) + + it('should return false if version is found but does not match a good version', async () => { + const stdout = 'version 1.2.3' + fsPathExistsStub.resolves(true) + execStub.resolves({ stdout }) + + const result = await binaryManager.isBinaryGood(binaryPath) + + expect(result).to.be.false + expect(fsPathExistsStub.calledOnce).to.be.true + expect(fsPathExistsStub.calledWith(binaryPath)).to.be.true + expect(execStub.calledOnce).to.be.true + expect(execStub.calledWith(execCommand)).to.be.true + }) + + it('should return true if version is found and matches a good version', async () => { + const stdout = 'version 6.1.2' + fsPathExistsStub.resolves(true) + execStub.resolves({ stdout }) + + const result = await binaryManager.isBinaryGood(binaryPath) + + expect(result).to.be.true + expect(fsPathExistsStub.calledOnce).to.be.true + expect(fsPathExistsStub.calledWith(binaryPath)).to.be.true + expect(execStub.calledOnce).to.be.true + expect(execStub.calledWith(execCommand)).to.be.true }) }) \ No newline at end of file From ba996c3b552ccd943609d470ed8bc3b62300ab84 Mon Sep 17 00:00:00 2001 From: bloodscript <steffen.mei@live.de> Date: Fri, 26 Jan 2024 20:12:37 +0100 Subject: [PATCH 0334/2145] translation wasnt accurate verschluesselung means encryption --- 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 c92e9296..a09bed8b 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -11,7 +11,7 @@ "ButtonAuthors": "Autoren", "ButtonBrowseForFolder": "Ordnersuche", "ButtonCancel": "Abbrechen", - "ButtonCancelEncode": "Abbruch der Verschlüsselung", + "ButtonCancelEncode": "Codierung abbrechen", "ButtonChangeRootPassword": "Hauptpasswort ändern", "ButtonCheckAndDownloadNewEpisodes": "Überprüfe & lade neue Episoden herunter", "ButtonChooseAFolder": "Wähle einen Ordner", From 455f27d4432a86cd461fdb7e9947ec9251a446f4 Mon Sep 17 00:00:00 2001 From: bloodscript <steffen.mei@live.de> Date: Fri, 26 Jan 2024 23:09:54 +0100 Subject: [PATCH 0335/2145] mainly changed usage formular wording for you to the more commonly used, also corrected some misspellings --- client/strings/de.json | 212 ++++++++++++++++++++--------------------- 1 file changed, 106 insertions(+), 106 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index a09bed8b..8b9ccfa1 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -47,13 +47,13 @@ "ButtonPlay": "Abspielen", "ButtonPlaying": "Spielt", "ButtonPlaylists": "Wiedergabelisten", - "ButtonPurgeAllCache": "Lösche alle Zwischenspeicher", - "ButtonPurgeItemsCache": "Lösche Medien-Zwischenspeicher", + "ButtonPurgeAllCache": "Cache leeren", + "ButtonPurgeItemsCache": "Lösche Medien-Cache", "ButtonPurgeMediaProgress": "Lösche Hörfortschritte", "ButtonQueueAddItem": "Zur Warteschlange hinzufügen", "ButtonQueueRemoveItem": "Aus der Warteschlange entfernen", "ButtonQuickMatch": "Schnellabgleich", - "ButtonRead": "Lese", + "ButtonRead": "Lesen", "ButtonRemove": "Löschen", "ButtonRemoveAll": "Alles löschen", "ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge", @@ -88,7 +88,7 @@ "ButtonViewAll": "Alles anzeigen", "ButtonYes": "Ja", "ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten", - "ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuchen Sie den Titel und oder den Autor zu updaten", + "ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuche den Titel und oder den Autor zu aktualisieren", "ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden", "HeaderAccount": "Konto", "HeaderAdvanced": "Erweitert", @@ -110,15 +110,15 @@ "HeaderEmail": "Email", "HeaderEmailSettings": "Email Einstellungen", "HeaderEpisodes": "Episoden", - "HeaderEreaderDevices": "Ereader Geräte", - "HeaderEreaderSettings": "Ereader Einstellungen", + "HeaderEreaderDevices": "E-Reader Geräte", + "HeaderEreaderSettings": "E-Reader Einstellungen", "HeaderFiles": "Dateien", "HeaderFindChapters": "Kapitel suchen", "HeaderIgnoredFiles": "Ignorierte Dateien", "HeaderItemFiles": "Medien-Dateien", "HeaderItemMetadataUtils": "Metadaten", "HeaderLastListeningSession": "Letzte Hörsitzung", - "HeaderLatestEpisodes": "Letzte Episoden", + "HeaderLatestEpisodes": "Neueste Episoden", "HeaderLibraries": "Bibliotheken", "HeaderLibraryFiles": "Alle Dateien", "HeaderLibraryStats": "Bibliotheksstatistiken", @@ -130,7 +130,7 @@ "HeaderManageTags": "Tags verwalten", "HeaderMapDetails": "Stapelverarbeitung", "HeaderMatch": "Metadaten", - "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", + "HeaderMetadataOrderOfPrecedence": "Metadata Rangfolge", "HeaderMetadataToEmbed": "Einzubettende Metadaten", "HeaderNewAccount": "Neues Konto", "HeaderNewLibrary": "Neue Bibliothek", @@ -138,9 +138,9 @@ "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung", "HeaderOpenRSSFeed": "RSS-Feed öffnen", "HeaderOtherFiles": "Sonstige Dateien", - "HeaderPasswordAuthentication": "Password Authentifizierung", + "HeaderPasswordAuthentication": "Passwort Authentifizierung", "HeaderPermissions": "Berechtigungen", - "HeaderPlayerQueue": "Spieler Warteschlange", + "HeaderPlayerQueue": "Player Warteschlange", "HeaderPlaylist": "Wiedergabeliste", "HeaderPlaylistItems": "Einträge in der Wiedergabeliste", "HeaderPodcastsToAdd": "Podcasts zum Hinzufügen", @@ -149,7 +149,7 @@ "HeaderRemoveEpisodes": "Lösche {0} Episoden", "HeaderRSSFeedGeneral": "RSS Details", "HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet", - "HeaderRSSFeeds": "RSS Feeds", + "HeaderRSSFeeds": "RSS-Feeds", "HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte", "HeaderSchedule": "Zeitplan", "HeaderScheduleLibraryScans": "Automatische Bibliotheksscans", @@ -160,7 +160,7 @@ "HeaderSettingsExperimental": "Experimentelle Funktionen", "HeaderSettingsGeneral": "Allgemein", "HeaderSettingsScanner": "Scanner", - "HeaderSleepTimer": "Einschlaf-Timer", + "HeaderSleepTimer": "Sleep-Timer", "HeaderStatsLargestItems": "Größte Medien", "HeaderStatsLongestItems": "Längste Medien (h)", "HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)", @@ -191,8 +191,8 @@ "LabelAll": "Alle", "LabelAllUsers": "Alle Benutzer", "LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen", - "LabelAllUsersIncludingGuests": "All Benutzer und Gäste", - "LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden", + "LabelAllUsersIncludingGuests": "Alle Benutzer und Gäste", + "LabelAlreadyInYourLibrary": "Bereits in der Bibliothek", "LabelAppend": "Anhängen", "LabelAuthor": "Autor", "LabelAuthorFirstLast": "Autor (Vorname Nachname)", @@ -204,7 +204,7 @@ "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", + "LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Registrieren", "LabelBackToUser": "Zurück zum Benutzer", "LabelBackupLocation": "Backup-Ort", "LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren", @@ -212,14 +212,14 @@ "LabelBackupsMaxBackupSize": "Maximale Sicherungsgröße (in GB)", "LabelBackupsMaxBackupSizeHelp": "Zum Schutz vor Fehlkonfigurationen schlagen Sicherungen fehl, wenn sie die konfigurierte Größe überschreiten.", "LabelBackupsNumberToKeep": "Anzahl der aufzubewahrenden Sicherungen", - "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.", + "LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn du bereits mehrere Sicherungen als die definierte max. Anzahl hast, solltest du diese manuell entfernen.", "LabelBitrate": "Bitrate", "LabelBooks": "Bücher", "LabelButtonText": "Button Text", "LabelChangePassword": "Passwort ändern", "LabelChannels": "Kanäle", "LabelChapters": "Kapitel", - "LabelChaptersFound": "gefundene Kapitel", + "LabelChaptersFound": "Gefundene Kapitel", "LabelChapterTitle": "Kapitelüberschrift", "LabelClickForMoreInfo": "Klicken für mehr Informationen", "LabelClosePlayer": "Player schließen", @@ -230,7 +230,7 @@ "LabelComplete": "Vollständig", "LabelConfirmPassword": "Passwort bestätigen", "LabelContinueListening": "Weiterhören", - "LabelContinueReading": "Lesen fortsetzen", + "LabelContinueReading": "Weiterlesen", "LabelContinueSeries": "Serien fortsetzen", "LabelCover": "Titelbild", "LabelCoverImageURL": "URL des Titelbildes", @@ -245,7 +245,7 @@ "LabelDeselectAll": "Alles abwählen", "LabelDevice": "Gerät", "LabelDeviceInfo": "Geräteinformationen", - "LabelDeviceIsAvailableTo": "Dem Geärt ist es möglich zu ...", + "LabelDeviceIsAvailableTo": "Dem Gerät ist es möglich zu ...", "LabelDirectory": "Verzeichnis", "LabelDiscFromFilename": "CD aus dem Dateinamen", "LabelDiscFromMetadata": "CD aus den Metadaten", @@ -258,9 +258,9 @@ "LabelEbooks": "E-Books", "LabelEdit": "Bearbeiten", "LabelEmail": "Email", - "LabelEmailSettingsFromAddress": "Von Address", - "LabelEmailSettingsSecure": "Sicherheit", - "LabelEmailSettingsSecureHelp": "Wenn \"true\", verwendet die Verbindung TLS, wenn sie eine Verbindung zum Server herstellt. Bei \"false\" wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen setzen Sie diesen Wert auf \"true\", wenn Sie eine Verbindung zu Port 465 herstellen. Für Port 587 oder 25 behalten Sie den Wert \"false\" bei. (von nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsFromAddress": "Von Addresse", + "LabelEmailSettingsSecure": "Sicher", + "LabelEmailSettingsSecureHelp": "Wenn \"an\", verwendet die Verbindung TLS, wenn du eine Verbindung zum Server herstellst. Bei \"aus\" wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen solltest du diesen Wert auf \"an\" schalten, wenn du eine Verbindung zu Port 465 herstellst. Für Port 587 oder 25 behalte den Wert \"aus\" bei. (von nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test Addresse", "LabelEmbeddedCover": "Eingebettetes Cover", "LabelEnable": "Aktivieren", @@ -278,7 +278,7 @@ "LabelFilename": "Dateiname", "LabelFilterByUser": "Nach Benutzern filtern", "LabelFindEpisodes": "Episoden suchen", - "LabelFinished": "beendet", + "LabelFinished": "Beendet", "LabelFolder": "Ordner", "LabelFolders": "Verzeichnisse", "LabelFontFamily": "Schriftfamilie", @@ -287,8 +287,8 @@ "LabelGenre": "Kategorie", "LabelGenres": "Kategorien", "LabelHardDeleteFile": "Datei dauerhaft löschen", - "LabelHasEbook": "mit E-Book", - "LabelHasSupplementaryEbook": "mit zusätlichem E-Book", + "LabelHasEbook": "E-Book verfügbar", + "LabelHasSupplementaryEbook": "Ergänzendes E-Book verfügbar", "LabelHighestPriority": "Höchste Priorität", "LabelHost": "Host", "LabelHour": "Stunde", @@ -311,9 +311,9 @@ "LabelItem": "Medium", "LabelLanguage": "Sprache", "LabelLanguageDefaultServer": "Standard-Server-Sprache", - "LabelLastBookAdded": "Zuletzt hinzugefügtes Medium", - "LabelLastBookUpdated": "Zuletzt aktualisiertes Medium", - "LabelLastSeen": "Zuletzt angesehen", + "LabelLastBookAdded": "Zuletzt hinzugefügtes Buch", + "LabelLastBookUpdated": "Zuletzt aktualisiertes Buch", + "LabelLastSeen": "Zuletzt gesehen", "LabelLastTime": "Letztes Mal", "LabelLastUpdate": "Letzte Aktualisierung", "LabelLayout": "Layout", @@ -330,13 +330,13 @@ "LabelLogLevelDebug": "Fehlersuche", "LabelLogLevelInfo": "Informationen", "LabelLogLevelWarn": "Warnungen", - "LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum", + "LabelLookForNewEpisodesAfterDate": "Suche nach neuen Episoden nach diesem Datum", "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", + "LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet", "LabelMediaPlayer": "Mediaplayer", "LabelMediaType": "Medientyp", - "LabelMetadataOrderOfPrecedenceDescription": "Eine Höhere Priorität Quelle für Metadaten wird die Metadaten aus eine Quelle mit niedrigerer Priorität überschreiben.", + "LabelMetadataOrderOfPrecedenceDescription": "Höher priorisierte Quelle für Metadaten überschreiben Metadaten aus Quellen die niedriger priorisiert sind.", "LabelMetadataProvider": "Metadatenanbieter", "LabelMetaTag": "Meta Schlagwort", "LabelMetaTags": "Meta Tags", @@ -344,21 +344,21 @@ "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.", + "LabelMobileRedirectURIsDescription": "Dies ist eine Whitelist gültiger Umleitungs-URIs für mobile Apps. Der Standardwert ist <code>audiobookshelf://oauth</code>, den du entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen kannst. Die Verwendung eines Sternchens (<code>*</code>) als alleiniger Eintrag erlaubt jede URI.", "LabelMore": "Mehr", - "LabelMoreInfo": "Mehr Info", + "LabelMoreInfo": "Mehr Infos", "LabelName": "Name", "LabelNarrator": "Erzähler", "LabelNarrators": "Erzähler", "LabelNew": "Neu", - "LabelNewestAuthors": "Neuste Autoren", + "LabelNewestAuthors": "Neueste Autoren", "LabelNewestEpisodes": "Neueste Episoden", "LabelNewPassword": "Neues Passwort", "LabelNextBackupDate": "Nächstes Sicherungsdatum", "LabelNextScheduledRun": "Nächster planmäßiger Durchlauf", "LabelNoEpisodesSelected": "Keine Episoden ausgewählt", - "LabelNotes": "Hinweise", - "LabelNotFinished": "nicht beendet", + "LabelNotes": "Notizen", + "LabelNotFinished": "Nicht beendet", "LabelNotificationAppriseURL": "Apprise URL(s)", "LabelNotificationAvailableVariables": "Verfügbare Variablen", "LabelNotificationBodyTemplate": "Textvorlage", @@ -371,7 +371,7 @@ "LabelNotStarted": "Nicht begonnen", "LabelNumberOfBooks": "Anzahl der Hörbücher", "LabelNumberOfEpisodes": "Anzahl der Episoden", - "LabelOpenRSSFeed": "Öffne RSS Feed", + "LabelOpenRSSFeed": "Öffne RSS-Feed", "LabelOverwrite": "Überschreiben", "LabelPassword": "Passwort", "LabelPath": "Pfad", @@ -391,14 +391,14 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)", "LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird", - "LabelPrimaryEbook": "Haupt-E-Book", + "LabelPrimaryEbook": "Primäres E-Book", "LabelProgress": "Fortschritt", "LabelProvider": "Anbieter", "LabelPubDate": "Veröffentlichungsdatum", "LabelPublisher": "Herausgeber", "LabelPublishYear": "Jahr", "LabelRead": "Lesen", - "LabelReadAgain": "Nocheinmal Lesen", + "LabelReadAgain": "Noch einmal Lesen", "LabelReadEbookWithoutProgress": "E-Book lesen und Fortschritt verwerfen", "LabelRecentlyAdded": "Kürzlich hinzugefügt", "LabelRecentSeries": "Aktuelle Serien", @@ -414,8 +414,8 @@ "LabelRSSFeedSlug": "RSS Feed Schlagwort", "LabelRSSFeedURL": "RSS Feed URL", "LabelSearchTerm": "Begriff suchen", - "LabelSearchTitle": "Titel", - "LabelSearchTitleOrASIN": "Titel oder ASIN", + "LabelSearchTitle": "Titel suchen", + "LabelSearchTitleOrASIN": "Titel oder ASIN suchen", "LabelSeason": "Staffel", "LabelSelectAllEpisodes": "Alle Episoden auswählen", "LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt", @@ -425,10 +425,10 @@ "LabelSeries": "Serien", "LabelSeriesName": "Serienname", "LabelSeriesProgress": "Serienfortschritt", - "LabelSetEbookAsPrimary": "Setzen als Hauptbuch", - "LabelSetEbookAsSupplementary": "Setzen als Ergänzung", - "LabelSettingsAudiobooksOnly": "nur Hörbücher", - "LabelSettingsAudiobooksOnlyHelp": "Wenn Sie diese Einstellung aktivieren, werden E-Book-Dateien ignoriert, es sei denn, sie befinden sich in einem Hörbuchordner. In diesem Fall werden sie als zusätzliche E-Books festgelegt", + "LabelSetEbookAsPrimary": "Als Hauptbuch setzen", + "LabelSetEbookAsSupplementary": "Als Ergänzung setzen", + "LabelSettingsAudiobooksOnly": "Nur Hörbücher", + "LabelSettingsAudiobooksOnlyHelp": "Wenn du diese Einstellung aktivierst, werden E-Book-Dateien ignoriert, es sei denn, sie befinden sich in einem Hörbuchordner. In diesem Fall werden sie als zusätzliche E-Books festgelegt", "LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden", "LabelSettingsChromecastSupport": "Chromecastunterstützung", "LabelSettingsDateFormat": "Datumsformat", @@ -439,11 +439,11 @@ "LabelSettingsEnableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek aktivieren", "LabelSettingsEnableWatcherHelp": "Aktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart", "LabelSettingsExperimentalFeatures": "Experimentelle Funktionen", - "LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.", + "LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen dein Feedback und deine Hilfe beim Testen. Klicke hier, um die Github-Diskussion zu öffnen.", "LabelSettingsFindCovers": "Suche Titelbilder", - "LabelSettingsFindCoversHelp": "Wenn Ihr Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer", - "LabelSettingsHideSingleBookSeries": "Ausblenden einzelzne Bücher", - "LabelSettingsHideSingleBookSeriesHelp": "Serien, die ein einzelnes Buch enthalten, werden in den Regalen der Serienseite und der Startseite ausgeblendet.", + "LabelSettingsFindCoversHelp": "Wenn dein Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer", + "LabelSettingsHideSingleBookSeries": "Ausblenden einzelner Bücher", + "LabelSettingsHideSingleBookSeriesHelp": "Serien, die nur ein einzelnes Buch enthalten, werden auf der Startseite und in der Serienansicht ausgeblendet.", "LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht", "LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht", "LabelSettingsParseSubtitles": "Analysiere Untertitel", @@ -455,7 +455,7 @@ "LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren", "LabelSettingsSortingIgnorePrefixesHelp": "Beispiel: für den Artikel \"der\" würde der Mediumtitel \"Der Buchtitel\" als \"Buchtitel, Der\" sortiert werden.", "LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder", - "LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1", + "LabelSettingsSquareBookCoversHelp": "Bevorzuge quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1", "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", @@ -463,7 +463,7 @@ "LabelSettingsTimeFormat": "Zeitformat", "LabelShowAll": "Alles anzeigen", "LabelSize": "Größe", - "LabelSleepTimer": "Einschlaf-Timer", + "LabelSleepTimer": "Sleep-Timer", "LabelSlug": "URL Teil", "LabelStart": "Start", "LabelStarted": "Gestartet", @@ -476,7 +476,7 @@ "LabelStatsDays": "Tage", "LabelStatsDaysListened": "Gehörte Tage", "LabelStatsHours": "Stunden", - "LabelStatsInARow": "nacheinander", + "LabelStatsInARow": "Nacheinander", "LabelStatsItemsFinished": "Gehörte Medien", "LabelStatsItemsInLibrary": "Bibliothekseinträge", "LabelStatsMinutes": "Minuten", @@ -492,8 +492,8 @@ "LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter", "LabelTasks": "Laufende Aufgaben", "LabelTheme": "Theme", - "LabelThemeDark": "Dark", - "LabelThemeLight": "Light", + "LabelThemeDark": "Dunkel", + "LabelThemeLight": "Hell", "LabelTimeBase": "Basiszeit", "LabelTimeListened": "Gehörte Zeit", "LabelTimeListenedToday": "Heute gehörte Zeit", @@ -503,12 +503,12 @@ "LabelToolsEmbedMetadata": "Metadaten einbetten", "LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.", "LabelToolsMakeM4b": "M4B-Datei erstellen", - "LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ....) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.", + "LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.", "LabelToolsSplitM4b": "M4B in MP3's aufteilen", "LabelToolsSplitM4bDescription": "Erstellt aus einer mit Metadaten und nach Kapiteln aufgeteilten M4B-Datei seperate MP3's mit eingebetteten Metadaten, Coverbild und Kapiteln.", "LabelTotalDuration": "Gesamtdauer", "LabelTotalTimeListened": "Gehörte Gesamtzeit", - "LabelTrackFromFilename": "Titel von Dateiname", + "LabelTrackFromFilename": "Titel aus Dateiname", "LabelTrackFromMetadata": "Titel aus Metadaten", "LabelTracks": "Dateien", "LabelTracksMultiTrack": "Mehrfachdatei", @@ -518,13 +518,13 @@ "LabelUnabridged": "Ungekürzt", "LabelUnknown": "Unbekannt", "LabelUpdateCover": "Titelbild aktualisieren", - "LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird", + "LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird", "LabelUpdatedAt": "Aktualisiert am", "LabelUpdateDetails": "Details aktualisieren", - "LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird", + "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": "Automatisches Abholden von Titel, Author und Serien", + "LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie", "LabelUseChapterTrack": "Kapiteldatei verwenden", "LabelUseFullTrack": "Gesamte Datei verwenden", "LabelUser": "Benutzer", @@ -533,17 +533,17 @@ "LabelVersion": "Version", "LabelViewBookmarks": "Lesezeichen anzeigen", "LabelViewChapters": "Kapitel anzeigen", - "LabelViewQueue": "Spieler-Warteschlange anzeigen", - "LabelVolume": "Volumen", + "LabelViewQueue": "Player-Warteschlange anzeigen", + "LabelVolume": "Lautstärke", "LabelWeekdaysToRun": "Wochentage für die Ausführung", - "LabelYourAudiobookDuration": "Laufzeit Ihres Mediums", + "LabelYourAudiobookDuration": "Laufzeit deines Mediums", "LabelYourBookmarks": "Lesezeichen", "LabelYourPlaylists": "Eigene Wiedergabelisten", "LabelYourProgress": "Fortschritt", "MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen", - "MessageAppriseDescription": "Um diese Funktion nutzen zu können, müssen Sie eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würden Sie <code>http://192.168.1.1:8337/notify</code> eingeben.", + "MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würdest du <code>http://192.168.1.1:8337/notify</code> eingeben.", "MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.", - "MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktivieren Sie die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.", + "MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.", "MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt", "MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"", "MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet", @@ -554,57 +554,57 @@ "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": "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?", - "MessageConfirmRemoveListeningSessions": "Sind Sie sicher, dass sie {0} Hörsitzungen enfernen möchten?", - "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?", + "MessageConfirmCloseFeed": "Feed wird geschlossen! Bist du dir sicher?", + "MessageConfirmDeleteBackup": "Sicherung für {0} wird gelöscht! Bist du dir sicher?", + "MessageConfirmDeleteFile": "Datei wird vom System gelöscht! Bist du dir sicher?", + "MessageConfirmDeleteLibrary": "Bibliothek \"{0}\" wird dauerhaft gelöscht! Bist du dir sicher?", + "MessageConfirmDeleteLibraryItem": "Bibliothekselement wird aus der Datenbank + Festplatte gelöscht? Bist du dir sicher?", + "MessageConfirmDeleteLibraryItems": "{0} Bibliothekselemente werden aus der Datenbank + Festplatte gelöscht? Bist du dir sicher?", + "MessageConfirmDeleteSession": "Sitzung wird gelöscht! Bist du dir sicher?", + "MessageConfirmForceReScan": "Scanvorgang erzwingen! Bist du dir sicher?", + "MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Bist du dir sicher?", + "MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Bist du dir sicher?", + "MessageConfirmMarkSeriesFinished": "Alle Medien dieser Reihe werden als abgeschlossen markiert! Bist du dir sicher?", + "MessageConfirmMarkSeriesNotFinished": "Alle Medien dieser Reihe werden als nicht abgeschlossen markiert! Bist du dir sicher?", + "MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achte darauf, dass du eine Sicherungskopie der Audiodateien besitzt. <br><br>Möchtest du fortfahren?", + "MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?", + "MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?", + "MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird gelöscht! Bist du dir sicher?", + "MessageConfirmRemoveEpisode": "Episode \"{0}\" wird geloscht! Bist du dir sicher?", + "MessageConfirmRemoveEpisodes": "{0} Episoden werden gelöscht! Bist du dir sicher?", + "MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?", + "MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird gelöscht! Bist du dir sicher?", + "MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Bist du dir sicher?", + "MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?", "MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.", "MessageConfirmRenameGenreWarning": "Warnung! Ein ähnliche Kategorie mit einem anderen Wortlaut existiert bereits: \"{0}\".", - "MessageConfirmRenameTag": "Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Sind Sie sicher?", + "MessageConfirmRenameTag": "Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?", "MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.", "MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".", - "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", + "MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Bist du dir sicher?", + "MessageConfirmSendEbookToDevice": "{0} E-Book \"{1}\" wird auf das Gerät \"{2}\" gesendet! Bist du dir sicher?", + "MessageDownloadingEpisode": "Episode wird heruntergeladen", + "MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge", "MessageEmbedFinished": "Einbettung abgeschlossen!", "MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen", "MessageFeedURLWillBe": "Feed-URL wird {0} sein", "MessageFetching": "Abrufen...", - "MessageForceReScanDescription": "durchsucht alle Dateien neu, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.", + "MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.", "MessageImportantNotice": "Wichtiger Hinweis!", "MessageInsertChapterBelow": "Kapitel unten einfügen", "MessageItemsSelected": "{0} ausgewählte Medien", "MessageItemsUpdated": "{0} Medien aktualisiert", - "MessageJoinUsOn": "Besuchen Sie uns auf", + "MessageJoinUsOn": "Besuche uns auf", "MessageListeningSessionsInTheLastYear": "{0} Ereignisse im letzten Jahr", "MessageLoading": "Laden...", "MessageLoadingFolders": "Lade Ordner...", "MessageM4BFailed": "M4B fehlgeschlagen!", "MessageM4BFinished": "M4B beendet!", - "MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben", + "MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu deinen vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben", "MessageMarkAllEpisodesFinished": "Alle Episoden als beendet markieren", "MessageMarkAllEpisodesNotFinished": "Alle Episoden als ungehört markieren", "MessageMarkAsFinished": "Als beendet markieren", - "MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren", + "MessageMarkAsNotFinished": "Als nicht beendet markieren", "MessageMatchBooksDescription": "Es wird versucht die Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen um leere Details und das Titelbild auszufüllen. Vorhandene Details werden nicht überschrieben.", "MessageNoAudioTracks": "Keine Audiodateien", "MessageNoAuthors": "Keine Autoren", @@ -637,7 +637,7 @@ "MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich", "MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig", "MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden", - "MessageOr": "oder", + "MessageOr": "Oder", "MessagePauseChapter": "Kapitelwiedergabe pausieren", "MessagePlayChapter": "Kapitelanfang anhören", "MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung", @@ -646,11 +646,11 @@ "MessageRemoveChapter": "Kapitel löschen", "MessageRemoveEpisodes": "Entferne {0} Episode(n)", "MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen", - "MessageRemoveUserWarning": "Benutzer \"{0}\" wird dauerhaft gelöscht! Sind Sie sicher?", - "MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf", - "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.", + "MessageRemoveUserWarning": "Benutzer \"{0}\" wird dauerhaft gelöscht! Bist du dir sicher?", + "MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken", + "MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Bist du dir sicher?", + "MessageRestoreBackupConfirm": "Bist du dir sicher, dass du die Sicherung wiederherstellen willst, 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 deinen Bibliotheksordnern verändert. Wenn du die Servereinstellungen aktiviert hast, um Cover und Metadaten in deinen Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.", "MessageSearchResultsFor": "Suchergebnisse für", "MessageSelected": "{0} ausgewählt", "MessageServerCouldNotBeReached": "Server kann nicht erreicht werden", @@ -663,15 +663,15 @@ "MessageValidCronExpression": "Gültiger Cron-Ausdruck", "MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert", "MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!", - "MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer", - "MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Mediums ist kürzer als die gefundene Dauer", + "MessageYourAudiobookDurationIsLonger": "Die Dauer deines Mediums ist länger als die gefundene Dauer", + "MessageYourAudiobookDurationIsShorter": "Die Dauer deines Mediums ist kürzer als die gefundene Dauer", "NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann", "NoteChapterEditorTimes": "Hinweis: Die Anfangszeit des ersten Kapitels muss bei 0:00 beginnen und die Anfangszeit des letzten Kapitels darf die Dauer des Mediums nicht überschreiten.", "NoteFolderPicker": "Hinweis: Bereits zugeordnete Ordner werden nicht angezeigt.", "NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.", - "NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.", + "NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere deiner Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.", "NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.", - "NoteUploaderOnlyAudioFiles": "Wenn Sie nur Audiodateien hochladen, wird jede Audiodatei als ein separates Medium behandelt.", + "NoteUploaderOnlyAudioFiles": "Wenn du nur Audiodateien hochlädst, wird jede Audiodatei als ein separates Medium behandelt.", "NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.", "PlaceholderNewCollection": "Neuer Sammlungsname", "PlaceholderNewFolderPath": "Neuer Ordnerpfad", @@ -739,7 +739,7 @@ "ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden", "ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen", "ToastSendEbookToDeviceFailed": "E-Book konnte nicht auf Gerät übertragen werden", - "ToastSendEbookToDeviceSuccess": "E-Book an Gerät senden \"{0}\"", + "ToastSendEbookToDeviceSuccess": "E-Book an Gerät \"{0}\" gesendet", "ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen", "ToastSeriesUpdateSuccess": "Serien aktualisiert", "ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden", From 68473ee3456424f894b5c339156c92f1afc5a304 Mon Sep 17 00:00:00 2001 From: bloodscript <steffen.mei@live.de> Date: Fri, 26 Jan 2024 23:11:38 +0100 Subject: [PATCH 0336/2145] added missing metadata translation --- 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 8b9ccfa1..4dbe5daf 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -130,7 +130,7 @@ "HeaderManageTags": "Tags verwalten", "HeaderMapDetails": "Stapelverarbeitung", "HeaderMatch": "Metadaten", - "HeaderMetadataOrderOfPrecedence": "Metadata Rangfolge", + "HeaderMetadataOrderOfPrecedence": "Metadaten Rangfolge", "HeaderMetadataToEmbed": "Einzubettende Metadaten", "HeaderNewAccount": "Neues Konto", "HeaderNewLibrary": "Neue Bibliothek", From 47999214bdba5a7b3650a5b1c80a183605c83c63 Mon Sep 17 00:00:00 2001 From: bloodscript <steffen.mei@live.de> Date: Fri, 26 Jan 2024 23:13:11 +0100 Subject: [PATCH 0337/2145] corrected misspelling of adress --- client/strings/de.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index 4dbe5daf..83c2c2d5 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -258,10 +258,10 @@ "LabelEbooks": "E-Books", "LabelEdit": "Bearbeiten", "LabelEmail": "Email", - "LabelEmailSettingsFromAddress": "Von Addresse", + "LabelEmailSettingsFromAddress": "Von Adresse", "LabelEmailSettingsSecure": "Sicher", "LabelEmailSettingsSecureHelp": "Wenn \"an\", verwendet die Verbindung TLS, wenn du eine Verbindung zum Server herstellst. Bei \"aus\" wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen solltest du diesen Wert auf \"an\" schalten, wenn du eine Verbindung zu Port 465 herstellst. Für Port 587 oder 25 behalte den Wert \"aus\" bei. (von nodemailer.com/smtp/#authentication)", - "LabelEmailSettingsTestAddress": "Test Addresse", + "LabelEmailSettingsTestAddress": "Test Adresse", "LabelEmbeddedCover": "Eingebettetes Cover", "LabelEnable": "Aktivieren", "LabelEnd": "Ende", From 056e62dce80409fa7ce657b6cec423919d5d9a40 Mon Sep 17 00:00:00 2001 From: bloodscript <steffen.mei@live.de> Date: Fri, 26 Jan 2024 23:15:27 +0100 Subject: [PATCH 0338/2145] added plural to metadata order hint --- 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 83c2c2d5..256a9826 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -336,7 +336,7 @@ "LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet", "LabelMediaPlayer": "Mediaplayer", "LabelMediaType": "Medientyp", - "LabelMetadataOrderOfPrecedenceDescription": "Höher priorisierte Quelle für Metadaten überschreiben Metadaten aus Quellen die niedriger priorisiert sind.", + "LabelMetadataOrderOfPrecedenceDescription": "Höher priorisierte Quellen für Metadaten überschreiben Metadaten aus Quellen die niedriger priorisiert sind.", "LabelMetadataProvider": "Metadatenanbieter", "LabelMetaTag": "Meta Schlagwort", "LabelMetaTags": "Meta Tags", From 0b334cf95705a91da34fcf7f8b60584baea40eae Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 26 Jan 2024 17:08:23 -0600 Subject: [PATCH 0339/2145] Add:Authentication setting to show a custom message on login #2552 --- .../modals/podcast/tabs/EpisodeDetails.vue | 2 +- client/components/ui/RichTextEditor.vue | 4 ++-- client/pages/config/authentication.vue | 18 ++++++++++++++++++ client/pages/login.vue | 5 +++++ server/objects/settings/ServerSettings.js | 11 ++++++++--- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/client/components/modals/podcast/tabs/EpisodeDetails.vue b/client/components/modals/podcast/tabs/EpisodeDetails.vue index d7130eba..720e1f75 100644 --- a/client/components/modals/podcast/tabs/EpisodeDetails.vue +++ b/client/components/modals/podcast/tabs/EpisodeDetails.vue @@ -19,7 +19,7 @@ <div class="w-full p-1"> <ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" /> </div> - <div class="w-full p-1 default-style"> + <div class="w-full p-1"> <ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" /> </div> </div> diff --git a/client/components/ui/RichTextEditor.vue b/client/components/ui/RichTextEditor.vue index 582f5e8f..48abd6f0 100644 --- a/client/components/ui/RichTextEditor.vue +++ b/client/components/ui/RichTextEditor.vue @@ -1,5 +1,5 @@ <template> - <div> + <div class="default-style"> <p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }"> {{ label }} </p> @@ -29,7 +29,7 @@ export default { config() { return { toolbar: { - getDefaultHTML: () => ` <div class="trix-button-row"> + getDefaultHTML: () => `<div class="trix-button-row"> <span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools"> <button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="#{lang.bold}" tabindex="-1">#{lang.bold}</button> <button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="#{lang.italic}" tabindex="-1">#{lang.italic}</button> diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 9e028307..199701b4 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -1,6 +1,18 @@ <template> <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"> + <ui-checkbox v-model="showCustomLoginMessage" checkbox-bg="bg" /> + <p class="text-lg pl-4">Custom Message on Login</p> + </div> + <transition name="slide"> + <div v-if="showCustomLoginMessage" class="w-full pt-4"> + <ui-rich-text-editor v-model="newAuthSettings.authLoginCustomMessage" /> + </div> + </transition> + </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="enableLocalAuth" checkbox-bg="bg" /> @@ -103,6 +115,7 @@ export default { return { enableLocalAuth: false, enableOpenIDAuth: false, + showCustomLoginMessage: false, savingSettings: false, newAuthSettings: {} } @@ -221,6 +234,10 @@ export default { return } + if (!this.showCustomLoginMessage || !this.newAuthSettings.authLoginCustomMessage?.trim()) { + this.newAuthSettings.authLoginCustomMessage = null + } + this.newAuthSettings.authActiveAuthMethods = [] if (this.enableLocalAuth) this.newAuthSettings.authActiveAuthMethods.push('local') if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid') @@ -250,6 +267,7 @@ export default { } this.enableLocalAuth = this.authMethods.includes('local') this.enableOpenIDAuth = this.authMethods.includes('openid') + this.showCustomLoginMessage = !!this.authSettings.authLoginCustomMessage } }, mounted() { diff --git a/client/pages/login.vue b/client/pages/login.vue index f7579dd6..c7efa0d0 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -28,6 +28,8 @@ <div class="w-full h-px bg-white bg-opacity-10 my-4" /> + <p v-if="loginCustomMessage" class="py-2 default-style mb-2" v-html="loginCustomMessage"></p> + <p v-if="error" class="text-error text-center py-2">{{ error }}</p> <form v-show="login_local" @submit.prevent="submitForm"> @@ -113,6 +115,9 @@ export default { }, openIDButtonText() { return this.authFormData?.authOpenIDButtonText || 'Login with OpenId' + }, + loginCustomMessage() { + return this.authFormData?.authLoginCustomMessage || null } }, methods: { diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 6e9d8456..d3a6bdf0 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -55,7 +55,7 @@ class ServerSettings { this.buildNumber = packageJson.buildNumber // Auth settings - // Active auth methodes + this.authLoginCustomMessage = null this.authActiveAuthMethods = ['local'] // openid settings @@ -113,6 +113,7 @@ class ServerSettings { this.version = settings.version || null this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 + this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.7.3 this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null @@ -201,6 +202,7 @@ class ServerSettings { logLevel: this.logLevel, version: this.version, buildNumber: this.buildNumber, + authLoginCustomMessage: this.authLoginCustomMessage, authActiveAuthMethods: this.authActiveAuthMethods, authOpenIDIssuerURL: this.authOpenIDIssuerURL, authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, @@ -213,7 +215,7 @@ 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 } } @@ -246,6 +248,7 @@ class ServerSettings { get authenticationSettings() { return { + authLoginCustomMessage: this.authLoginCustomMessage, authActiveAuthMethods: this.authActiveAuthMethods, authOpenIDIssuerURL: this.authOpenIDIssuerURL, authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL, @@ -264,7 +267,9 @@ class ServerSettings { } get authFormData() { - const clientFormData = {} + const clientFormData = { + authLoginCustomMessage: this.authLoginCustomMessage + } if (this.authActiveAuthMethods.includes('openid')) { clientFormData.authOpenIDButtonText = this.authOpenIDButtonText clientFormData.authOpenIDAutoLaunch = this.authOpenIDAutoLaunch From e43c4f082eecb1a5a9c5a2c2fe2031311c99a25a Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 26 Jan 2024 17:22:37 -0600 Subject: [PATCH 0340/2145] Fix:Rich text editor labels and add translations --- client/components/ui/RichTextEditor.vue | 22 +++++++++++----------- client/strings/cs.json | 9 +++++++++ client/strings/da.json | 9 +++++++++ client/strings/de.json | 9 +++++++++ client/strings/en-us.json | 9 +++++++++ client/strings/es.json | 9 +++++++++ client/strings/fr.json | 9 +++++++++ client/strings/gu.json | 9 +++++++++ client/strings/hi.json | 9 +++++++++ client/strings/hr.json | 9 +++++++++ client/strings/it.json | 9 +++++++++ client/strings/lt.json | 9 +++++++++ client/strings/nl.json | 9 +++++++++ client/strings/no.json | 9 +++++++++ client/strings/pl.json | 9 +++++++++ client/strings/ru.json | 9 +++++++++ client/strings/sv.json | 9 +++++++++ client/strings/zh-cn.json | 9 +++++++++ 18 files changed, 164 insertions(+), 11 deletions(-) diff --git a/client/components/ui/RichTextEditor.vue b/client/components/ui/RichTextEditor.vue index 48abd6f0..c5ae6d83 100644 --- a/client/components/ui/RichTextEditor.vue +++ b/client/components/ui/RichTextEditor.vue @@ -31,29 +31,29 @@ export default { toolbar: { getDefaultHTML: () => `<div class="trix-button-row"> <span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools"> - <button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="#{lang.bold}" tabindex="-1">#{lang.bold}</button> - <button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="#{lang.italic}" tabindex="-1">#{lang.italic}</button> - <button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="#{lang.strike}" tabindex="-1">#{lang.strike}</button> - <button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="#{lang.link}" tabindex="-1">#{lang.link}</button> + <button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="${this.$strings.LabelFontBold}" tabindex="-1">${this.$strings.LabelFontBold}</button> + <button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="${this.$strings.LabelFontItalic}" tabindex="-1">${this.$strings.LabelFontItalic}</button> + <button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="${this.$strings.LabelFontStrikethrough}" tabindex="-1">${this.$strings.LabelFontStrikethrough}</button> + <button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="${this.$strings.LabelTextEditorLink}" tabindex="-1">${this.$strings.LabelTextEditorLink}</button> </span> <span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools"> - <button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="#{lang.bullets}" tabindex="-1">#{lang.bullets}</button> - <button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="#{lang.numbers}" tabindex="-1">#{lang.numbers}</button> + <button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="${this.$strings.LabelTextEditorBulletedList}" tabindex="-1">${this.$strings.LabelTextEditorBulletedList}</button> + <button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="${this.$strings.LabelTextEditorNumberedList}" tabindex="-1">${this.$strings.LabelTextEditorNumberedList}</button> </span> <span class="trix-button-group-spacer"></span> <span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools"> - <button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="#{lang.undo}" tabindex="-1">#{lang.undo}</button> - <button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="#{lang.redo}" tabindex="-1">#{lang.redo}</button> + <button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="${this.$strings.LabelUndo}" tabindex="-1">${this.$strings.LabelUndo}</button> + <button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="${this.$strings.LabelRedo}" tabindex="-1">${this.$strings.LabelRedo}</button> </span> </div> <div class="trix-dialogs" data-trix-dialogs> <div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href"> <div class="trix-dialog__link-fields"> - <input type="url" name="href" class="trix-input trix-input--dialog" placeholder="#{lang.urlPlaceholder}" aria-label="#{lang.url}" required data-trix-input> + <input type="url" name="href" class="trix-input trix-input--dialog" placeholder="" aria-label="URL" required data-trix-input> <div class="trix-button-group"> - <input type="button" class="trix-button trix-button--dialog" value="#{lang.link}" data-trix-method="setAttribute"> - <input type="button" class="trix-button trix-button--dialog" value="#{lang.unlink}" data-trix-method="removeAttribute"> + <input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorLink}" data-trix-method="setAttribute"> + <input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorUnlink}" data-trix-method="removeAttribute"> </div> </div> </div> diff --git a/client/strings/cs.json b/client/strings/cs.json index 18e35553..1e761882 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -281,8 +281,11 @@ "LabelFinished": "Dokončeno", "LabelFolder": "Složka", "LabelFolders": "Složky", + "LabelFontBold": "Bold", "LabelFontFamily": "Rodina písem", + "LabelFontItalic": "Italic", "LabelFontScale": "Měřítko písma", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Formát", "LabelGenre": "Žánr", "LabelGenres": "Žánry", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Nedávno přidané", "LabelRecentSeries": "Nedávné série", "LabelRecommended": "Doporučeno", + "LabelRedo": "Redo", "LabelRegion": "Region", "LabelReleaseDate": "Datum vydání", "LabelRemoveCover": "Odstranit obálku", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Značky přístupné uživateli", "LabelTagsNotAccessibleToUser": "Značky nepřístupné uživateli", "LabelTasks": "Spuštěné Úlohy", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Téma", "LabelThemeDark": "Tmavé", "LabelThemeLight": "Světlé", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Jedna stopa", "LabelType": "Typ", "LabelUnabridged": "Nezkráceno", + "LabelUndo": "Undo", "LabelUnknown": "Neznámý", "LabelUpdateCover": "Aktualizovat obálku", "LabelUpdateCoverHelp": "Povolit přepsání existujících obálek pro vybrané knihy, pokud je nalezena shoda", diff --git a/client/strings/da.json b/client/strings/da.json index 0d3cfafa..07d7d5a3 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -281,8 +281,11 @@ "LabelFinished": "Færdig", "LabelFolder": "Mappe", "LabelFolders": "Mapper", + "LabelFontBold": "Bold", "LabelFontFamily": "Fontfamilie", + "LabelFontItalic": "Italic", "LabelFontScale": "Skriftstørrelse", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Format", "LabelGenre": "Genre", "LabelGenres": "Genrer", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Senest tilføjet", "LabelRecentSeries": "Seneste serie", "LabelRecommended": "Anbefalet", + "LabelRedo": "Redo", "LabelRegion": "Region", "LabelReleaseDate": "Udgivelsesdato", "LabelRemoveCover": "Fjern omslag", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Mærker tilgængelige for bruger", "LabelTagsNotAccessibleToUser": "Mærker ikke tilgængelige for bruger", "LabelTasks": "Kører opgaver", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Tema", "LabelThemeDark": "Mørk", "LabelThemeLight": "Lys", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Enkeltspors", "LabelType": "Type", "LabelUnabridged": "Uforkortet", + "LabelUndo": "Undo", "LabelUnknown": "Ukendt", "LabelUpdateCover": "Opdater omslag", "LabelUpdateCoverHelp": "Tillad overskrivning af eksisterende omslag for de valgte bøger, når der findes en match", diff --git a/client/strings/de.json b/client/strings/de.json index 256a9826..49037f4b 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -281,8 +281,11 @@ "LabelFinished": "Beendet", "LabelFolder": "Ordner", "LabelFolders": "Verzeichnisse", + "LabelFontBold": "Bold", "LabelFontFamily": "Schriftfamilie", + "LabelFontItalic": "Italic", "LabelFontScale": "Schriftgröße", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Format", "LabelGenre": "Kategorie", "LabelGenres": "Kategorien", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Kürzlich hinzugefügt", "LabelRecentSeries": "Aktuelle Serien", "LabelRecommended": "Empfohlen", + "LabelRedo": "Redo", "LabelRegion": "Region", "LabelReleaseDate": "Veröffentlichungsdatum", "LabelRemoveCover": "Lösche Titelbild", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter", "LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter", "LabelTasks": "Laufende Aufgaben", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Theme", "LabelThemeDark": "Dunkel", "LabelThemeLight": "Hell", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Einzeldatei", "LabelType": "Typ", "LabelUnabridged": "Ungekürzt", + "LabelUndo": "Undo", "LabelUnknown": "Unbekannt", "LabelUpdateCover": "Titelbild aktualisieren", "LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 51efaef5..e3349d1f 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -281,8 +281,11 @@ "LabelFinished": "Finished", "LabelFolder": "Folder", "LabelFolders": "Folders", + "LabelFontBold": "Bold", "LabelFontFamily": "Font family", + "LabelFontItalic": "Italic", "LabelFontScale": "Font scale", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Format", "LabelGenre": "Genre", "LabelGenres": "Genres", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Recently Added", "LabelRecentSeries": "Recent Series", "LabelRecommended": "Recommended", + "LabelRedo": "Redo", "LabelRegion": "Region", "LabelReleaseDate": "Release Date", "LabelRemoveCover": "Remove cover", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Tags Accessible to User", "LabelTagsNotAccessibleToUser": "Tags not Accessible to User", "LabelTasks": "Tasks Running", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Theme", "LabelThemeDark": "Dark", "LabelThemeLight": "Light", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Single-track", "LabelType": "Type", "LabelUnabridged": "Unabridged", + "LabelUndo": "Undo", "LabelUnknown": "Unknown", "LabelUpdateCover": "Update Cover", "LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located", diff --git a/client/strings/es.json b/client/strings/es.json index f6edc557..24496683 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -281,8 +281,11 @@ "LabelFinished": "Terminado", "LabelFolder": "Carpeta", "LabelFolders": "Carpetas", + "LabelFontBold": "Bold", "LabelFontFamily": "Familia tipográfica", + "LabelFontItalic": "Italic", "LabelFontScale": "Tamaño de Fuente", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Formato", "LabelGenre": "Genero", "LabelGenres": "Géneros", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Agregado Recientemente", "LabelRecentSeries": "Series Recientes", "LabelRecommended": "Recomendados", + "LabelRedo": "Redo", "LabelRegion": "Región", "LabelReleaseDate": "Fecha de Estreno", "LabelRemoveCover": "Remover Portada", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Etiquetas Accessibles al Usuario", "LabelTagsNotAccessibleToUser": "Etiquetas no Accesibles al Usuario", "LabelTasks": "Tareas Corriendo", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Tema", "LabelThemeDark": "Oscuro", "LabelThemeLight": "Claro", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Una pista", "LabelType": "Tipo", "LabelUnabridged": "No Abreviado", + "LabelUndo": "Undo", "LabelUnknown": "Desconocido", "LabelUpdateCover": "Actualizar Portada", "LabelUpdateCoverHelp": "Permitir sobrescribir las portadas existentes de los libros seleccionados cuando sean encontradas.", diff --git a/client/strings/fr.json b/client/strings/fr.json index 6cf51544..2fe6f6e4 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -281,8 +281,11 @@ "LabelFinished": "Terminé le", "LabelFolder": "Dossier", "LabelFolders": "Dossiers", + "LabelFontBold": "Bold", "LabelFontFamily": "Polices de caractères", + "LabelFontItalic": "Italic", "LabelFontScale": "Taille de la police de caractère", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Format", "LabelGenre": "Genre", "LabelGenres": "Genres", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Derniers ajouts", "LabelRecentSeries": "Séries récentes", "LabelRecommended": "Recommandé", + "LabelRedo": "Redo", "LabelRegion": "Région", "LabelReleaseDate": "Date de parution", "LabelRemoveCover": "Supprimer la couverture", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur", "LabelTagsNotAccessibleToUser": "Étiquettes non accessibles à l’utilisateur", "LabelTasks": "Tâches en cours", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Thème", "LabelThemeDark": "Sombre", "LabelThemeLight": "Clair", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Piste simple", "LabelType": "Type", "LabelUnabridged": "Version intégrale", + "LabelUndo": "Undo", "LabelUnknown": "Inconnu", "LabelUpdateCover": "Mettre à jour la couverture", "LabelUpdateCoverHelp": "Autoriser la mise à jour de la couverture existante lorsqu’une correspondance est trouvée", diff --git a/client/strings/gu.json b/client/strings/gu.json index 9775ffad..349ff8e5 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -281,8 +281,11 @@ "LabelFinished": "Finished", "LabelFolder": "Folder", "LabelFolders": "Folders", + "LabelFontBold": "Bold", "LabelFontFamily": "ફોન્ટ કુટુંબ", + "LabelFontItalic": "Italic", "LabelFontScale": "Font scale", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Format", "LabelGenre": "Genre", "LabelGenres": "Genres", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Recently Added", "LabelRecentSeries": "Recent Series", "LabelRecommended": "Recommended", + "LabelRedo": "Redo", "LabelRegion": "Region", "LabelReleaseDate": "Release Date", "LabelRemoveCover": "Remove cover", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Tags Accessible to User", "LabelTagsNotAccessibleToUser": "Tags not Accessible to User", "LabelTasks": "Tasks Running", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Theme", "LabelThemeDark": "Dark", "LabelThemeLight": "Light", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Single-track", "LabelType": "Type", "LabelUnabridged": "Unabridged", + "LabelUndo": "Undo", "LabelUnknown": "Unknown", "LabelUpdateCover": "Update Cover", "LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located", diff --git a/client/strings/hi.json b/client/strings/hi.json index 205d674d..fcc174d9 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -281,8 +281,11 @@ "LabelFinished": "Finished", "LabelFolder": "Folder", "LabelFolders": "Folders", + "LabelFontBold": "Bold", "LabelFontFamily": "फुहारा परिवार", + "LabelFontItalic": "Italic", "LabelFontScale": "Font scale", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Format", "LabelGenre": "Genre", "LabelGenres": "Genres", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Recently Added", "LabelRecentSeries": "Recent Series", "LabelRecommended": "Recommended", + "LabelRedo": "Redo", "LabelRegion": "Region", "LabelReleaseDate": "Release Date", "LabelRemoveCover": "Remove cover", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Tags Accessible to User", "LabelTagsNotAccessibleToUser": "Tags not Accessible to User", "LabelTasks": "Tasks Running", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Theme", "LabelThemeDark": "Dark", "LabelThemeLight": "Light", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Single-track", "LabelType": "Type", "LabelUnabridged": "Unabridged", + "LabelUndo": "Undo", "LabelUnknown": "Unknown", "LabelUpdateCover": "Update Cover", "LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located", diff --git a/client/strings/hr.json b/client/strings/hr.json index daa2479c..0aa37771 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -281,8 +281,11 @@ "LabelFinished": "Finished", "LabelFolder": "Folder", "LabelFolders": "Folderi", + "LabelFontBold": "Bold", "LabelFontFamily": "Font family", + "LabelFontItalic": "Italic", "LabelFontScale": "Font scale", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Format", "LabelGenre": "Genre", "LabelGenres": "Žanrovi", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Nedavno dodano", "LabelRecentSeries": "Nedavne serije", "LabelRecommended": "Recommended", + "LabelRedo": "Redo", "LabelRegion": "Regija", "LabelReleaseDate": "Datum izlaska", "LabelRemoveCover": "Remove cover", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Tags dostupni korisniku", "LabelTagsNotAccessibleToUser": "Tags not Accessible to User", "LabelTasks": "Tasks Running", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Theme", "LabelThemeDark": "Dark", "LabelThemeLight": "Light", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Single-track", "LabelType": "Tip", "LabelUnabridged": "Unabridged", + "LabelUndo": "Undo", "LabelUnknown": "Nepoznato", "LabelUpdateCover": "Aktualiziraj Cover", "LabelUpdateCoverHelp": "Dozvoli postavljanje novog covera za odabrane knjige nakon što je match pronađen.", diff --git a/client/strings/it.json b/client/strings/it.json index 9aedfb22..077a225d 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -281,8 +281,11 @@ "LabelFinished": "Finita", "LabelFolder": "Cartella", "LabelFolders": "Cartelle", + "LabelFontBold": "Bold", "LabelFontFamily": "Font family", + "LabelFontItalic": "Italic", "LabelFontScale": "Dimensione Font", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Formato", "LabelGenre": "Genere", "LabelGenres": "Generi", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Aggiunti Recentemente", "LabelRecentSeries": "Serie Recenti", "LabelRecommended": "Raccomandati", + "LabelRedo": "Redo", "LabelRegion": "Regione", "LabelReleaseDate": "Data Release", "LabelRemoveCover": "Rimuovi cover", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Tags permessi agli Utenti", "LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti", "LabelTasks": "Processi in esecuzione", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Tema", "LabelThemeDark": "Scuro", "LabelThemeLight": "Chiaro", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Traccia-singola", "LabelType": "Tipo", "LabelUnabridged": "Integrale", + "LabelUndo": "Undo", "LabelUnknown": "Sconosciuto", "LabelUpdateCover": "Aggiornamento Cover", "LabelUpdateCoverHelp": "Consenti la sovrascrittura delle copertine esistenti per i libri selezionati quando viene trovata una corrispondenza", diff --git a/client/strings/lt.json b/client/strings/lt.json index e1f6ec30..43cfb8ee 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -281,8 +281,11 @@ "LabelFinished": "Baigta", "LabelFolder": "Aplankas", "LabelFolders": "Aplankai", + "LabelFontBold": "Bold", "LabelFontFamily": "Famiglia di font", + "LabelFontItalic": "Italic", "LabelFontScale": "Šrifto mastelis", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Formatas", "LabelGenre": "Žanras", "LabelGenres": "Žanrai", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Neseniai pridėta", "LabelRecentSeries": "Naujausios serijos", "LabelRecommended": "Rekomenduojama", + "LabelRedo": "Redo", "LabelRegion": "Regionas", "LabelReleaseDate": "Išleidimo data", "LabelRemoveCover": "Pašalinti viršelį", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Žymos, pasiekiamos vartotojui", "LabelTagsNotAccessibleToUser": "Žymos, nepasiekiamos vartotojui", "LabelTasks": "Vykdomos užduotys", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Tema", "LabelThemeDark": "Tamsi", "LabelThemeLight": "Šviesi", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Vienas takelis", "LabelType": "Tipas", "LabelUnabridged": "Neprikurptas", + "LabelUndo": "Undo", "LabelUnknown": "Nežinoma", "LabelUpdateCover": "Atnaujinti viršelį", "LabelUpdateCoverHelp": "Leisti perrašyti esamus viršelius pasirinktoms knygoms, kai yra rasta atitikmenų", diff --git a/client/strings/nl.json b/client/strings/nl.json index 3d1888ef..bffe649d 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -281,8 +281,11 @@ "LabelFinished": "Voltooid", "LabelFolder": "Map", "LabelFolders": "Mappen", + "LabelFontBold": "Bold", "LabelFontFamily": "Lettertypefamilie", + "LabelFontItalic": "Italic", "LabelFontScale": "Lettertype schaal", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Formaat", "LabelGenre": "Genre", "LabelGenres": "Genres", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Recent toegevoegd", "LabelRecentSeries": "Recente series", "LabelRecommended": "Aangeraden", + "LabelRedo": "Redo", "LabelRegion": "Regio", "LabelReleaseDate": "Verschijningsdatum", "LabelRemoveCover": "Verwijder cover", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Tags toegankelijk voor de gebruiker", "LabelTagsNotAccessibleToUser": "Tags niet toegankelijk voor de gebruiker", "LabelTasks": "Lopende taken", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Thema", "LabelThemeDark": "Donker", "LabelThemeLight": "Licht", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Enkele track", "LabelType": "Type", "LabelUnabridged": "Onverkort", + "LabelUndo": "Undo", "LabelUnknown": "Onbekend", "LabelUpdateCover": "Cover bijwerken", "LabelUpdateCoverHelp": "Sta overschrijven van bestaande covers toe voor de geselecteerde boeken wanneer een match is gevonden", diff --git a/client/strings/no.json b/client/strings/no.json index 49aceffe..109b2200 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -281,8 +281,11 @@ "LabelFinished": "Fullført", "LabelFolder": "Mappe", "LabelFolders": "Mapper", + "LabelFontBold": "Bold", "LabelFontFamily": "Fontfamilie", + "LabelFontItalic": "Italic", "LabelFontScale": "Font størrelse", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Format", "LabelGenre": "Sjanger", "LabelGenres": "Sjangers", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Nylig lagt til", "LabelRecentSeries": "Nylige serier", "LabelRecommended": "Anbefalte", + "LabelRedo": "Redo", "LabelRegion": "Region", "LabelReleaseDate": "Utgivelsesdato", "LabelRemoveCover": "Fjern omslag", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Tagger tilgjengelig for bruker", "LabelTagsNotAccessibleToUser": "Tagger ikke tilgjengelig for bruker", "LabelTasks": "Oppgaver som kjører", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Tema", "LabelThemeDark": "Mørk", "LabelThemeLight": "Lys", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Enkelspor", "LabelType": "Type", "LabelUnabridged": "Uavkortet", + "LabelUndo": "Undo", "LabelUnknown": "Ukjent", "LabelUpdateCover": "Oppdater omslag", "LabelUpdateCoverHelp": "Tillat overskriving av eksisterende omslag for de valgte bøkene når en lik bok er funnet", diff --git a/client/strings/pl.json b/client/strings/pl.json index 799d8c39..c304f187 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -281,8 +281,11 @@ "LabelFinished": "Zakończone", "LabelFolder": "Folder", "LabelFolders": "Foldery", + "LabelFontBold": "Bold", "LabelFontFamily": "Rodzina czcionek", + "LabelFontItalic": "Italic", "LabelFontScale": "Font scale", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Format", "LabelGenre": "Gatunek", "LabelGenres": "Gatunki", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Niedawno dodany", "LabelRecentSeries": "Ostatnie serie", "LabelRecommended": "Recommended", + "LabelRedo": "Redo", "LabelRegion": "Region", "LabelReleaseDate": "Data wydania", "LabelRemoveCover": "Remove cover", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika", "LabelTagsNotAccessibleToUser": "Tags not Accessible to User", "LabelTasks": "Tasks Running", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Theme", "LabelThemeDark": "Dark", "LabelThemeLight": "Light", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Single-track", "LabelType": "Typ", "LabelUnabridged": "Unabridged", + "LabelUndo": "Undo", "LabelUnknown": "Nieznany", "LabelUpdateCover": "Zaktalizuj odkładkę", "LabelUpdateCoverHelp": "Umożliwienie nadpisania istniejących okładek dla wybranych książek w przypadku znalezienia dopasowania", diff --git a/client/strings/ru.json b/client/strings/ru.json index aa1cd7e5..e1a2bf5e 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -281,8 +281,11 @@ "LabelFinished": "Закончен", "LabelFolder": "Папка", "LabelFolders": "Папки", + "LabelFontBold": "Bold", "LabelFontFamily": "Семейство шрифтов", + "LabelFontItalic": "Italic", "LabelFontScale": "Масштаб шрифта", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Формат", "LabelGenre": "Жанр", "LabelGenres": "Жанры", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Недавно добавленные", "LabelRecentSeries": "Последние серии", "LabelRecommended": "Рекомендованное", + "LabelRedo": "Redo", "LabelRegion": "Регион", "LabelReleaseDate": "Дата выхода", "LabelRemoveCover": "Удалить обложку", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Теги доступные для пользователя", "LabelTagsNotAccessibleToUser": "Теги не доступные для пользователя", "LabelTasks": "Запущенные задачи", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Тема", "LabelThemeDark": "Темная", "LabelThemeLight": "Светлая", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Один трек", "LabelType": "Тип", "LabelUnabridged": "Полное издание", + "LabelUndo": "Undo", "LabelUnknown": "Неизвестно", "LabelUpdateCover": "Обновить обложку", "LabelUpdateCoverHelp": "Позволяет перезаписывать существующие обложки для выбранных книг если будут найдены", diff --git a/client/strings/sv.json b/client/strings/sv.json index 8832a0aa..40594dd3 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -281,8 +281,11 @@ "LabelFinished": "Avslutad", "LabelFolder": "Mapp", "LabelFolders": "Mappar", + "LabelFontBold": "Bold", "LabelFontFamily": "Teckensnittsfamilj", + "LabelFontItalic": "Italic", "LabelFontScale": "Teckensnittsskala", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "Format", "LabelGenre": "Genre", "LabelGenres": "Genrer", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "Nyligen tillagd", "LabelRecentSeries": "Senaste serier", "LabelRecommended": "Rekommenderad", + "LabelRedo": "Redo", "LabelRegion": "Region", "LabelReleaseDate": "Utgivningsdatum", "LabelRemoveCover": "Ta bort omslag", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "Taggar tillgängliga för användaren", "LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren", "LabelTasks": "Körande uppgifter", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Tema", "LabelThemeDark": "Mörkt", "LabelThemeLight": "Ljust", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "Enspårigt", "LabelType": "Typ", "LabelUnabridged": "Oavkortad", + "LabelUndo": "Undo", "LabelUnknown": "Okänd", "LabelUpdateCover": "Uppdatera omslag", "LabelUpdateCoverHelp": "Tillåt överskrivning av befintliga omslag för de valda böckerna när en matchning hittas", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 05ef483b..9212b998 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -281,8 +281,11 @@ "LabelFinished": "已听完", "LabelFolder": "文件夹", "LabelFolders": "文件夹", + "LabelFontBold": "Bold", "LabelFontFamily": "字体系列", + "LabelFontItalic": "Italic", "LabelFontScale": "字体比例", + "LabelFontStrikethrough": "Strikethrough", "LabelFormat": "编码格式", "LabelGenre": "流派", "LabelGenres": "流派", @@ -403,6 +406,7 @@ "LabelRecentlyAdded": "最近添加", "LabelRecentSeries": "最近添加系列", "LabelRecommended": "推荐内容", + "LabelRedo": "Redo", "LabelRegion": "区域", "LabelReleaseDate": "发布日期", "LabelRemoveCover": "移除封面", @@ -491,6 +495,10 @@ "LabelTagsAccessibleToUser": "用户可访问的标签", "LabelTagsNotAccessibleToUser": "用户无法访问标签", "LabelTasks": "正在运行的任务", + "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorUnlink": "Unlink", "LabelTheme": "主题", "LabelThemeDark": "黑暗", "LabelThemeLight": "明亮", @@ -516,6 +524,7 @@ "LabelTracksSingleTrack": "单轨", "LabelType": "类型", "LabelUnabridged": "未删节", + "LabelUndo": "Undo", "LabelUnknown": "未知", "LabelUpdateCover": "更新封面", "LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面", From 0b6a8a964125271b23ce601aa075f4b4d92fb369 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 27 Jan 2024 10:50:44 -0600 Subject: [PATCH 0341/2145] Update:Remove 64x64 app icon from PWA manifest so that only the SVG is available #2520 --- client/nuxt.config.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/client/nuxt.config.js b/client/nuxt.config.js index 1f02bcaf..6506f168 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -108,11 +108,6 @@ module.exports = { { src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg', sizes: "any" - }, - { - src: (process.env.ROUTER_BASE_PATH || '') + '/icon64.png', - type: "image/png", - sizes: "64x64" } ] }, From 24da859975cc0558abeb4ee6bb25d9cdfc1eca50 Mon Sep 17 00:00:00 2001 From: Lena During <info@teawork.de> Date: Sun, 28 Jan 2024 16:06:07 +0100 Subject: [PATCH 0342/2145] Update de.json --- client/strings/de.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index 49037f4b..fe5b3e02 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -70,7 +70,7 @@ "ButtonScan": "Partial-Scan (nur geänderte/neue Medien)", "ButtonScanLibrary": "Bibliothek scannen", "ButtonSearch": "Suchen", - "ButtonSelectFolderPath": "Auswahl Ordnerpfad", + "ButtonSelectFolderPath": "Ordnerpfad auswählen", "ButtonSeries": "Serien", "ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien", "ButtonShiftTimes": "Zeitverschiebung", @@ -758,4 +758,4 @@ "ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen", "ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden", "ToastUserDeleteSuccess": "Benutzer gelöscht" -} \ No newline at end of file +} From c6c67078b8f63f635e68c6422bf861dc2e84903a Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 28 Jan 2024 16:00:26 -0600 Subject: [PATCH 0343/2145] Update:PWA manifest icon to include PNG #2520 --- client/nuxt.config.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/nuxt.config.js b/client/nuxt.config.js index 6506f168..1736e3f6 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -107,7 +107,12 @@ module.exports = { icons: [ { src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg', - sizes: "any" + sizes: 'any' + }, + { + src: (process.env.ROUTER_BASE_PATH || '') + '/icon192.png', + type: 'image/png', + sizes: 'any' } ] }, From bedb260b00aa9e9e7d1475caf2c8f93227dc857a Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 28 Jan 2024 16:02:02 -0600 Subject: [PATCH 0344/2145] Update:Epub ereader allow scripted content --- client/components/readers/EpubReader.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index aa11d162..3941c0d8 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -316,6 +316,7 @@ export default { reader.rendition = reader.book.renderTo('viewer', { width: this.readerWidth, height: this.readerHeight * 0.8, + allowScriptedContent: true, spread: 'auto', snap: true, manager: 'continuous', From 8c703859a01a5fd2f89188a4326366864df70276 Mon Sep 17 00:00:00 2001 From: ipcintron <ipcintron1@gmail.com> Date: Mon, 29 Jan 2024 13:18:58 -0600 Subject: [PATCH 0345/2145] added raw cover --- client/store/globals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/store/globals.js b/client/store/globals.js index 961dd52e..32d58c96 100644 --- a/client/store/globals.js +++ b/client/store/globals.js @@ -99,7 +99,7 @@ export const getters = { 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}` + return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}` }, getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, timestamp = null, raw = false) => { const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg` From 295ca3d9a21d41674927e45967f325313a68cffa Mon Sep 17 00:00:00 2001 From: Spenser Bushey <spenser@spenserbushey.com> Date: Tue, 30 Jan 2024 09:15:50 -0800 Subject: [PATCH 0346/2145] Return png from AudiobookCovers.com Changes AudiobookCovers.com provider to return the full size png file from the server. The original file url has the incorrect content-type header set, which caused issues downloading new cover images. --- server/providers/AudiobookCovers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/providers/AudiobookCovers.js b/server/providers/AudiobookCovers.js index edb1b199..53ae0508 100644 --- a/server/providers/AudiobookCovers.js +++ b/server/providers/AudiobookCovers.js @@ -14,7 +14,7 @@ class AudiobookCovers { Logger.error('[AudiobookCovers] Cover search error', error) return [] }) - return items.map(item => ({ cover: item.filename })) + return items.map(item => ({ cover: item.versions.png.original })) } } From ebe511404a2eed977ea7fc7b649b33480eef65e6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 31 Jan 2024 17:23:16 -0600 Subject: [PATCH 0347/2145] Remove updateMedia endpoint cover cache purge --- server/controllers/LibraryItemController.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index b0ecf446..26b375a0 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -124,11 +124,6 @@ class LibraryItemController { const libraryItem = req.libraryItem const mediaPayload = req.body - // Item has cover and update is removing cover so purge it from cache - if (libraryItem.media.coverPath && (mediaPayload.coverPath === '' || mediaPayload.coverPath === null)) { - await CacheManager.purgeCoverCache(libraryItem.id) - } - // Book specific if (libraryItem.isBook) { await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId) From 2ebdb4482673371db2adb94c9e828ed209cc9a37 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 1 Feb 2024 12:03:12 +0200 Subject: [PATCH 0348/2145] Merge cover and media update in Match.vue into a single /media API call --- client/components/modals/item/tabs/Match.vue | 21 ++++-------------- server/controllers/LibraryItemController.js | 23 +++++++++++++------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index b57e9612..ef4a7753 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -533,24 +533,11 @@ export default { // Persist in local storage localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage)) - if (updatePayload.metadata.cover) { - const coverPayload = { - url: updatePayload.metadata.cover - } - const success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => { - console.error('Failed to update', error) - return false - }) - if (success) { - this.$toast.success(this.$strings.ToastItemCoverUpdateSuccess) - } else { - this.$toast.error(this.$strings.ToastItemCoverUpdateFailed) - } - console.log('Updated cover') - delete updatePayload.metadata.cover - } - if (Object.keys(updatePayload).length) { + if (updatePayload.metadata.cover) { + updatePayload.url = updatePayload.metadata.cover + delete updatePayload.metadata.cover + } const mediaUpdatePayload = updatePayload const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => { console.error('Failed to update', error) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 26b375a0..dfa1daea 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -124,6 +124,11 @@ class LibraryItemController { const libraryItem = req.libraryItem const mediaPayload = req.body + if (mediaPayload.url) { + await LibraryItemController.prototype.uploadCover.bind(this)(req, res, false) + if (res.writableEnded) return + } + // Book specific if (libraryItem.isBook) { await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId) @@ -146,7 +151,7 @@ class LibraryItemController { seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id)) } - const hasUpdates = libraryItem.media.update(mediaPayload) + const hasUpdates = libraryItem.media.update(mediaPayload) || mediaPayload.url if (hasUpdates) { libraryItem.updatedAt = Date.now() @@ -171,7 +176,7 @@ class LibraryItemController { } // POST: api/items/:id/cover - async uploadCover(req, res) { + async uploadCover(req, res, updateAndReturnJson = true) { if (!req.user.canUpload) { Logger.warn('User attempted to upload a cover without permission', req.user) return res.sendStatus(403) @@ -196,12 +201,14 @@ class LibraryItemController { return res.status(500).send('Unknown error occurred') } - await Database.updateLibraryItem(libraryItem) - SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) - res.json({ - success: true, - cover: result.cover - }) + if (updateAndReturnJson) { + await Database.updateLibraryItem(libraryItem) + SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) + res.json({ + success: true, + cover: result.cover + }) + } } // PATCH: api/items/:id/cover From e2bb0cfb7cb7fb4587eaf0ff346296c37c24b315 Mon Sep 17 00:00:00 2001 From: KeyboardHammer <alexhammer8484@gmail.com> Date: Sat, 3 Feb 2024 21:48:35 -0600 Subject: [PATCH 0349/2145] add sorting to author page --- client/components/app/BookShelfToolbar.vue | 30 +++++++++++++++++++ .../pages/library/_library/authors/index.vue | 23 +++++++++++++- client/store/user.js | 4 ++- server/controllers/LibraryController.js | 1 + 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index bd31768b..74ab02f1 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -98,6 +98,9 @@ <template v-else-if="page === 'authors'"> <div class="flex-grow" /> <ui-btn v-if="userCanUpdate && authors && authors.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn> + + <!-- author sort select --> + <controls-sort-select v-if="authors && authors.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" /> </template> </div> </div> @@ -183,6 +186,30 @@ export default { } ] }, + authorSortItems() { + return [ + { + text: this.$strings.LabelAuthorFirstLast, + value: 'name' + }, + { + text: this.$strings.LabelAuthorLastFirst, + value: 'lastFirst' + }, + { + text: this.$strings.LabelNumberOfBooks, + value: 'numBooks' + }, + { + text: this.$strings.LabelAddedAt, + value: 'addedAt' + }, + { + text: this.$strings.LabelLastUpdated, + value: 'lastUpdated' + } + ] + }, userIsAdminOrUp() { return this.$store.getters['user/getIsAdminOrUp'] }, @@ -455,6 +482,9 @@ export default { updateCollapseBookSeries() { this.saveSettings() }, + updateAuthorSort() { + this.saveSettings() + }, saveSettings() { this.$store.dispatch('user/updateUserSettings', this.settings) }, diff --git a/client/pages/library/_library/authors/index.vue b/client/pages/library/_library/authors/index.vue index 1f8e385b..a81865a5 100644 --- a/client/pages/library/_library/authors/index.vue +++ b/client/pages/library/_library/authors/index.vue @@ -48,9 +48,12 @@ export default { }, methods: { async init() { + this.settings = { ...this.$store.state.user.settings } this.authors = await this.$axios .$get(`/api/libraries/${this.currentLibraryId}/authors`) - .then((response) => response.authors) + .then((response) => { + return this.sortAuthors(response.authors) + }) .catch((error) => { console.error('Failed to load authors', error) return [] @@ -78,15 +81,33 @@ export default { }, editAuthor(author) { this.$store.commit('globals/showEditAuthorModal', author) + }, + sortAuthors(authors) { + const sortProp = this.settings.authorSortBy + const bDesc = this.settings.authorSortDesc === true ? -1 : 1 + return authors.sort((a, b) => { + if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') { + return a[sortProp] > b[sortProp] ? 1 * bDesc : -1 * bDesc + } + return a[sortProp].localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc + }) + }, + settingsUpdated(settings) { + for (const key in settings) { + this.settings[key] = settings[key] + } + this.sortAuthors(this.authors) } }, mounted() { this.init() + this.$eventBus.$on('user-settings', this.settingsUpdated) this.$root.socket.on('author_added', this.authorAdded) this.$root.socket.on('author_updated', this.authorUpdated) this.$root.socket.on('author_removed', this.authorRemoved) }, beforeDestroy() { + this.$eventBus.$off('user-settings', this.settingsUpdated) this.$root.socket.off('author_added', this.authorAdded) this.$root.socket.off('author_updated', this.authorUpdated) this.$root.socket.off('author_removed', this.authorRemoved) diff --git a/client/store/user.js b/client/store/user.js index 85babb09..40a8813f 100644 --- a/client/store/user.js +++ b/client/store/user.js @@ -11,7 +11,9 @@ export const state = () => ({ useChapterTrack: false, seriesSortBy: 'name', seriesSortDesc: false, - seriesFilterBy: 'all' + seriesFilterBy: 'all', + authorSortBy: 'name', + authorSortDesc: false, } }) diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 70baff85..b2500d4f 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -636,6 +636,7 @@ class LibraryController { for (const author of authors) { const oldAuthor = author.getOldAuthor().toJSON() oldAuthor.numBooks = author.books.length + oldAuthor.lastFirst = author.lastFirst oldAuthors.push(oldAuthor) } From d24427aad8dd613d2035e0424930f705527b185a Mon Sep 17 00:00:00 2001 From: KeyboardHammer <alexhammer8484@gmail.com> Date: Sat, 3 Feb 2024 22:04:40 -0600 Subject: [PATCH 0350/2145] fix property --- client/components/app/BookShelfToolbar.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 74ab02f1..9064c914 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -205,8 +205,8 @@ export default { value: 'addedAt' }, { - text: this.$strings.LabelLastUpdated, - value: 'lastUpdated' + text: this.$strings.LabelUpdatedAt, + value: 'updatedAt' } ] }, From 19af7454f219106a717966522e2b7e1c968c4a28 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 7 Feb 2024 20:57:50 +0200 Subject: [PATCH 0351/2145] Force Update LibraryItem model updatedAt refresh (fixes #2593) --- server/models/LibraryItem.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index ee8a4bb8..c8971e91 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -372,6 +372,9 @@ class LibraryItem extends Model { if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) { Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`) hasLibraryItemUpdates = true + if (key === 'updatedAt') { + libraryItemExpanded.changed('updatedAt', true) + } } } if (hasLibraryItemUpdates) { @@ -399,6 +402,7 @@ class LibraryItem extends Model { isInvalid: !!oldLibraryItem.isInvalid, mtime: oldLibraryItem.mtimeMs, ctime: oldLibraryItem.ctimeMs, + updatedAt: oldLibraryItem.updatedAt, birthtime: oldLibraryItem.birthtimeMs, size: oldLibraryItem.size, lastScan: oldLibraryItem.lastScan, From 2093468c92fb2bc51722b28bfb3fcd61bb9a6643 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 8 Feb 2024 19:12:35 -0600 Subject: [PATCH 0352/2145] Fix:Local playback sessions not persisting the last updatedAt value --- server/models/PlaybackSession.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/models/PlaybackSession.js b/server/models/PlaybackSession.js index 3bb8653b..cca73cc5 100644 --- a/server/models/PlaybackSession.js +++ b/server/models/PlaybackSession.js @@ -118,7 +118,9 @@ class PlaybackSession extends Model { static createFromOld(oldPlaybackSession) { const playbackSession = this.getFromOld(oldPlaybackSession) - return this.create(playbackSession) + return this.create(playbackSession, { + silent: true + }) } static updateFromOld(oldPlaybackSession) { @@ -126,7 +128,8 @@ class PlaybackSession extends Model { return this.update(playbackSession, { where: { id: playbackSession.id - } + }, + silent: true }) } From fe1e0749a2646bbd8e3e91781e1319d14c569fc8 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 8 Feb 2024 19:12:59 -0600 Subject: [PATCH 0353/2145] Update:Listening sessions table rows per page text wrapping --- client/pages/config/sessions.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index d90d849d..162c7747 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -84,7 +84,7 @@ <div class="flex items-center my-2"> <div class="flex-grow" /> <div class="hidden sm:inline-flex items-center"> - <p class="text-sm">{{ $strings.LabelRowsPerPage }}</p> + <p class="text-sm whitespace-nowrap">{{ $strings.LabelRowsPerPage }}</p> <ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" /> </div> <div class="inline-flex items-center"> From 0cf2f8885ec34f85433f254c0e1ae23007be4a6c Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 11 Feb 2024 16:48:16 -0600 Subject: [PATCH 0354/2145] Add custom metadata provider controller, update model, move to item metadata utils --- client/components/app/ConfigSideNav.vue | 5 - client/components/app/SettingsContent.vue | 1 + .../modals/AddCustomMetadataProviderModal.vue | 83 +++++------ client/components/modals/item/tabs/Match.vue | 15 +- .../tables/CustomMetadataProviderTable.vue | 139 ++++++++---------- client/layouts/default.vue | 14 +- .../config/custom-metadata-providers.vue | 43 ------ .../custom-metadata-providers.vue | 74 ++++++++++ .../config/item-metadata-utils/index.vue | 6 + client/store/libraries.js | 3 + client/store/scanners.js | 73 +++++---- client/strings/en-us.json | 4 +- server/Database.js | 39 ----- .../CustomMetadataProviderController.js | 117 +++++++++++++++ server/controllers/LibraryController.js | 36 +++-- server/controllers/MiscController.js | 90 ------------ server/controllers/SessionController.js | 2 +- server/finders/BookFinder.js | 11 +- server/models/CustomMetadataProvider.js | 61 +++++++- server/providers/CustomProviderAdapter.js | 40 +++-- server/routers/ApiRouter.js | 13 +- 21 files changed, 496 insertions(+), 373 deletions(-) delete mode 100644 client/pages/config/custom-metadata-providers.vue create mode 100644 client/pages/config/item-metadata-utils/custom-metadata-providers.vue create mode 100644 server/controllers/CustomMetadataProviderController.js diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index e253d1ae..c2db0725 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -109,11 +109,6 @@ export default { id: 'config-authentication', title: this.$strings.HeaderAuthentication, path: '/config/authentication' - }, - { - id: 'config-custom-metadata-providers', - title: this.$strings.HeaderCustomMetadataProviders, - path: '/config/custom-metadata-providers' } ] diff --git a/client/components/app/SettingsContent.vue b/client/components/app/SettingsContent.vue index c78873e3..ec129ebc 100644 --- a/client/components/app/SettingsContent.vue +++ b/client/components/app/SettingsContent.vue @@ -1,6 +1,7 @@ <template> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> <div class="flex items-center mb-2"> + <slot name="header-prefix"></slot> <h1 class="text-xl">{{ headerText }}</h1> <slot name="header-items"></slot> diff --git a/client/components/modals/AddCustomMetadataProviderModal.vue b/client/components/modals/AddCustomMetadataProviderModal.vue index 1b9f930c..bd870890 100644 --- a/client/components/modals/AddCustomMetadataProviderModal.vue +++ b/client/components/modals/AddCustomMetadataProviderModal.vue @@ -6,18 +6,23 @@ </div> </template> <form @submit.prevent="submitForm"> - <div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh"> + <div class="px-4 w-full flex items-center text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh"> <div class="w-full p-8"> - <div class="w-full mb-4"> - <ui-text-input-with-label v-model="newName" :label="$strings.LabelName" /> + <div class="flex mb-2"> + <div class="w-3/4 p-1"> + <ui-text-input-with-label v-model="newName" :label="$strings.LabelName" /> + </div> + <div class="w-1/4 p-1"> + <ui-text-input-with-label value="Book" readonly :label="$strings.LabelMediaType" /> + </div> </div> - <div class="w-full mb-4"> - <ui-text-input-with-label v-model="newUrl" :label="$strings.LabelUrl" /> + <div class="w-full mb-2 p-1"> + <ui-text-input-with-label v-model="newUrl" label="URL" /> </div> - <div class="w-full mb-4"> - <ui-text-input-with-label v-model="newApiKey" :label="$strings.LabelApiKey" type="password" /> + <div class="w-full mb-2 p-1"> + <ui-text-input-with-label v-model="newAuthHeaderValue" :label="'Authorization Header Value'" type="password" /> </div> - <div class="flex pt-4 px-2"> + <div class="flex px-1 pt-4"> <div class="flex-grow" /> <ui-btn color="success" type="submit">{{ $strings.ButtonAdd }}</ui-btn> </div> @@ -30,14 +35,14 @@ <script> export default { props: { - value: Boolean, + value: Boolean }, data() { return { processing: false, - newName: "", - newUrl: "", - newApiKey: "", + newName: '', + newUrl: '', + newAuthHeaderValue: '' } }, watch: { @@ -57,46 +62,42 @@ export default { set(val) { this.$emit('input', val) } - }, + } }, methods: { - close() { - // Force close when navigating - used in the table - if (this.$refs.modal) this.$refs.modal.setHide() - }, submitForm() { - if (!this.newName || !this.newUrl || !this.newApiKey) { - this.$toast.error('Must add name, url and API key') + if (!this.newName || !this.newUrl) { + this.$toast.error('Must add name and url') return } this.processing = true this.$axios - .$patch('/api/custom-metadata-providers/admin', { - name: this.newName, - url: this.newUrl, - apiKey: this.newApiKey, - }) - .then((data) => { - this.processing = false - if (data.error) { - this.$toast.error(`Failed to add provider: ${data.error}`) - } else { - this.$toast.success('New provider added') - this.show = false - } - }) - .catch((error) => { - this.processing = false - console.error('Failed to add provider', error) - this.$toast.error('Failed to add provider') - }) + .$post('/api/custom-metadata-providers', { + name: this.newName, + url: this.newUrl, + mediaType: 'book', // Currently only supporting book mediaType + authHeaderValue: this.newAuthHeaderValue + }) + .then((data) => { + this.$emit('added', data.provider) + this.$toast.success('New provider added') + this.show = false + }) + .catch((error) => { + const errorMsg = error.response?.data || 'Unknown error' + console.error('Failed to add provider', error) + this.$toast.error('Failed to add provider: ' + errorMsg) + }) + .finally(() => { + this.processing = false + }) }, init() { this.processing = false - this.newName = "" - this.newUrl = "" - this.newApiKey = "" + this.newName = '' + this.newUrl = '' + this.newAuthHeaderValue = '' } }, mounted() {} diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index b57e9612..8aa312b5 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -328,6 +328,17 @@ export default { console.error('PersistProvider', error) } }, + getDefaultBookProvider() { + let provider = localStorage.getItem('book-provider') + if (!provider) return 'google' + // Validate book provider + if (!this.$store.getters['scanners/checkBookProviderExists'](provider)) { + console.error('Stored book provider does not exist', provider) + localStorage.removeItem('book-provider') + return 'google' + } + return provider + }, getSearchQuery() { if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}` var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}` @@ -434,7 +445,9 @@ export default { this.searchTitle = this.libraryItem.media.metadata.title this.searchAuthor = this.libraryItem.media.metadata.authorName || '' if (this.isPodcast) this.provider = 'itunes' - else this.provider = localStorage.getItem('book-provider') || 'google' + else { + this.provider = this.getDefaultBookProvider() + } // Prefer using ASIN if set and using audible provider if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) { diff --git a/client/components/tables/CustomMetadataProviderTable.vue b/client/components/tables/CustomMetadataProviderTable.vue index 8104cede..13414471 100644 --- a/client/components/tables/CustomMetadataProviderTable.vue +++ b/client/components/tables/CustomMetadataProviderTable.vue @@ -1,96 +1,73 @@ <template> - <div> - <div class="text-center"> - <table id="providers"> - <tr> - <th>{{ $strings.LabelName }}</th> - <th>{{ $strings.LabelUrl }}</th> - <th>{{ $strings.LabelApiKey }}</th> - <th class="w-12"></th> - </tr> - <tr v-for="provider in providers" :key="provider.id"> - <td class="text-sm">{{ provider.name }}</td> - <td class="text-sm">{{ provider.url }}</td> - <td class="text-sm"> - <span class="custom-provider-api-key">{{ provider.apiKey }}</span> - </td> - <td class="py-0"> - <div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="removeProvider(provider)"> - <button type="button" :aria-label="$strings.ButtonDelete" class="material-icons text-base">delete</button> - </div> - </td> - </tr> - </table> + <div class="min-h-40"> + <table v-if="providers.length" id="providers"> + <tr> + <th>{{ $strings.LabelName }}</th> + <th>URL</th> + <th>Authorization Header Value</th> + <th class="w-12"></th> + </tr> + <tr v-for="provider in providers" :key="provider.id"> + <td class="text-sm">{{ provider.name }}</td> + <td class="text-sm">{{ provider.url }}</td> + <td class="text-sm"> + <span v-if="provider.authHeaderValue" class="custom-provider-api-key">{{ provider.authHeaderValue }}</span> + </td> + <td class="py-0"> + <div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="removeProvider(provider)"> + <button type="button" :aria-label="$strings.ButtonDelete" class="material-icons text-base">delete</button> + </div> + </td> + </tr> + </table> + <div v-else-if="!processing" class="text-center py-8"> + <p class="text-lg">No custom metadata providers</p> + </div> + + <div v-if="processing" class="absolute inset-0 h-full flex items-center justify-center bg-black/40 rounded-md"> + <ui-loading-indicator /> </div> </div> </template> <script> export default { + props: { + providers: { + type: Array, + default: () => [] + }, + processing: Boolean + }, data() { - return { - providers: [], - } + return {} }, methods: { - addedProvider(provider) { - if (!Array.isArray(this.providers)) return - - this.providers.push(provider) - }, - removedProvider(provider) { - this.providers = this.providers.filter((p) => p.id !== provider.id) - }, removeProvider(provider) { - this.$axios - .$delete(`/api/custom-metadata-providers/admin/${provider.id}`) - .then((data) => { - if (data.error) { - this.$toast.error(`Failed to remove provider: ${data.error}`) - } else { - this.$toast.success('Provider removed') - } - }) - .catch((error) => { - console.error('Failed to remove provider', error) - this.$toast.error('Failed to remove provider') - }) - }, - loadProviders() { - this.$axios.$get('/api/custom-metadata-providers/admin') - .then((res) => { - this.providers = res.providers - }) - .catch((error) => { - console.error('Failed', error) - }) - }, - init(attempts = 0) { - if (!this.$root.socket) { - if (attempts > 10) { - return console.error('Failed to setup socket listeners') - } - setTimeout(() => { - this.init(++attempts) - }, 250) - return - } - this.$root.socket.on('custom_metadata_provider_added', this.addedProvider) - this.$root.socket.on('custom_metadata_provider_removed', this.removedProvider) - } - }, - mounted() { - this.loadProviders() - this.init() - }, - beforeDestroy() { - if (this.$refs.addModal) { - this.$refs.addModal.close() - } + const payload = { + message: `Are you sure you want remove custom metadata provider "${provider.name}"?`, + callback: (confirmed) => { + if (confirmed) { + this.$emit('update:processing', true) - if (this.$root.socket) { - this.$root.socket.off('custom_metadata_provider_added', this.addedProvider) - this.$root.socket.off('custom_metadata_provider_removed', this.removedProvider) + this.$axios + .$delete(`/api/custom-metadata-providers/${provider.id}`) + .then(() => { + this.$toast.success('Provider removed') + this.$emit('removed', provider.id) + }) + .catch((error) => { + console.error('Failed to remove provider', error) + this.$toast.error('Failed to remove provider') + }) + .finally(() => { + this.$emit('update:processing', false) + }) + } + }, + type: 'yesNo' + } + this.$store.commit('globals/setConfirmPrompt', payload) } } } diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 1d33c44c..41d4943a 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -328,8 +328,13 @@ export default { this.$store.commit('libraries/setEReaderDevices', data.ereaderDevices) }, - customMetadataProvidersChanged() { - this.$store.dispatch('scanners/reFetchCustom') + customMetadataProviderAdded(provider) { + if (!provider?.id) return + this.$store.commit('scanners/addCustomMetadataProvider', provider) + }, + customMetadataProviderRemoved(provider) { + if (!provider?.id) return + this.$store.commit('scanners/removeCustomMetadataProvider', provider) }, initializeSocket() { this.socket = this.$nuxtSocket({ @@ -411,8 +416,8 @@ export default { this.socket.on('admin_message', this.adminMessageEvt) // Custom metadata provider Listeners - this.socket.on('custom_metadata_provider_added', this.customMetadataProvidersChanged) - this.socket.on('custom_metadata_provider_removed', this.customMetadataProvidersChanged) + this.socket.on('custom_metadata_provider_added', this.customMetadataProviderAdded) + this.socket.on('custom_metadata_provider_removed', this.customMetadataProviderRemoved) }, showUpdateToast(versionData) { var ignoreVersion = localStorage.getItem('ignoreVersion') @@ -548,7 +553,6 @@ export default { window.addEventListener('keydown', this.keyDown) this.$store.dispatch('libraries/load') - this.$store.dispatch('scanners/reFetchCustom') this.initLocalStorage() diff --git a/client/pages/config/custom-metadata-providers.vue b/client/pages/config/custom-metadata-providers.vue deleted file mode 100644 index 9f394eae..00000000 --- a/client/pages/config/custom-metadata-providers.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> - <div id="authentication-settings"> - <app-settings-content :header-text="$strings.HeaderCustomMetadataProviders"> - <template #header-items> - <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2"> - <a href="https://www.audiobookshelf.org/guides/#" 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="setShowAddModal">{{ $strings.ButtonAdd }}</ui-btn> - </template> - - <tables-custom-metadata-provider-table class="pt-2" /> - <modals-add-custom-metadata-provider-modal ref="addModal" v-model="showAddModal" /> - </app-settings-content> - </div> -</template> - -<script> -export default { - async asyncData({ store, redirect }) { - if (!store.getters['user/getIsAdminOrUp']) { - redirect('/') - return - } - return {} - }, - data() { - return { - showAddModal: false, - } - }, - methods: { - setShowAddModal() { - this.showAddModal = true - } - } -} -</script> - -<style></style> diff --git a/client/pages/config/item-metadata-utils/custom-metadata-providers.vue b/client/pages/config/item-metadata-utils/custom-metadata-providers.vue new file mode 100644 index 00000000..66581dae --- /dev/null +++ b/client/pages/config/item-metadata-utils/custom-metadata-providers.vue @@ -0,0 +1,74 @@ +<template> + <div class="relative"> + <app-settings-content :header-text="$strings.HeaderCustomMetadataProviders"> + <template #header-prefix> + <nuxt-link to="/config/item-metadata-utils" class="w-8 h-8 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center mr-2"> + <span class="material-icons text-2xl">arrow_back</span> + </nuxt-link> + </template> + <template #header-items> + <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2"> + <a href="https://www.audiobookshelf.org/guides/#" 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="setShowAddModal">{{ $strings.ButtonAdd }}</ui-btn> + </template> + + <tables-custom-metadata-provider-table :providers="providers" :processing.sync="processing" class="pt-2" @removed="providerRemoved" /> + <modals-add-custom-metadata-provider-modal ref="addModal" v-model="showAddModal" @added="providerAdded" /> + </app-settings-content> + </div> +</template> + +<script> +export default { + async asyncData({ store, redirect }) { + if (!store.getters['user/getIsAdminOrUp']) { + redirect('/') + return + } + return {} + }, + data() { + return { + showAddModal: false, + processing: false, + providers: [] + } + }, + methods: { + providerRemoved(providerId) { + this.providers = this.providers.filter((p) => p.id !== providerId) + }, + providerAdded(provider) { + this.providers.push(provider) + }, + setShowAddModal() { + this.showAddModal = true + }, + loadProviders() { + this.processing = true + this.$axios + .$get('/api/custom-metadata-providers') + .then((res) => { + this.providers = res.providers + }) + .catch((error) => { + console.error('Failed', error) + this.$toast.error('Failed to load custom metadata providers') + }) + .finally(() => { + this.processing = false + }) + } + }, + mounted() { + this.loadProviders() + } +} +</script> + +<style></style> diff --git a/client/pages/config/item-metadata-utils/index.vue b/client/pages/config/item-metadata-utils/index.vue index 3a12261b..7d0ba068 100644 --- a/client/pages/config/item-metadata-utils/index.vue +++ b/client/pages/config/item-metadata-utils/index.vue @@ -13,6 +13,12 @@ <span class="material-icons">arrow_forward</span> </div> </nuxt-link> + <nuxt-link to="/config/item-metadata-utils/custom-metadata-providers" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 my-2"> + <div class="flex justify-between"> + <p>{{ $strings.HeaderCustomMetadataProviders }}</p> + <span class="material-icons">arrow_forward</span> + </div> + </nuxt-link> </app-settings-content> </div> </template> diff --git a/client/store/libraries.js b/client/store/libraries.js index 8771ebcf..1d13d632 100644 --- a/client/store/libraries.js +++ b/client/store/libraries.js @@ -113,6 +113,7 @@ export const actions = { const library = data.library const filterData = data.filterdata const issues = data.issues || 0 + const customMetadataProviders = data.customMetadataProviders || [] const numUserPlaylists = data.numUserPlaylists dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true }) @@ -126,6 +127,8 @@ export const actions = { commit('setLibraryIssues', issues) commit('setLibraryFilterData', filterData) commit('setNumUserPlaylists', numUserPlaylists) + commit('scanners/setCustomMetadataProviders', customMetadataProviders, { root: true }) + commit('setCurrentLibrary', libraryId) return data }) diff --git a/client/store/scanners.js b/client/store/scanners.js index 32878a6a..2d3d465c 100644 --- a/client/store/scanners.js +++ b/client/store/scanners.js @@ -71,35 +71,56 @@ export const state = () => ({ ] }) -export const getters = {} - -export const actions = { - reFetchCustom({ dispatch, commit }) { - return this.$axios - .$get(`/api/custom-metadata-providers`) - .then((data) => { - const providers = data.providers - - commit('setCustomProviders', providers) - return data - }) - .catch((error) => { - console.error('Failed', error) - return false - }) +export const getters = { + checkBookProviderExists: state => (providerValue) => { + return state.providers.some(p => p.value === providerValue) }, + checkPodcastProviderExists: state => (providerValue) => { + return state.podcastProviders.some(p => p.value === providerValue) + } } +export const actions = {} + export const mutations = { - setCustomProviders(state, providers) { - // clear previous values, and add new values to the end - state.providers = state.providers.filter((p) => !p.value.startsWith("custom-")); - state.providers = [ - ...state.providers, - ...providers.map((p) => {return { - text: p.name, - value: p.slug, - }}) - ] + addCustomMetadataProvider(state, provider) { + if (provider.mediaType === 'book') { + if (state.providers.some(p => p.value === provider.slug)) return + state.providers.push({ + text: provider.name, + value: provider.slug + }) + } else { + if (state.podcastProviders.some(p => p.value === provider.slug)) return + state.podcastProviders.push({ + text: provider.name, + value: provider.slug + }) + } }, + removeCustomMetadataProvider(state, provider) { + if (provider.mediaType === 'book') { + state.providers = state.providers.filter(p => p.value !== provider.slug) + } else { + state.podcastProviders = state.podcastProviders.filter(p => p.value !== provider.slug) + } + }, + setCustomMetadataProviders(state, providers) { + if (!providers?.length) return + + const mediaType = providers[0].mediaType + if (mediaType === 'book') { + // clear previous values, and add new values to the end + state.providers = state.providers.filter((p) => !p.value.startsWith('custom-')) + state.providers = [ + ...state.providers, + ...providers.map((p) => ({ + text: p.name, + value: p.slug + })) + ] + } else { + // Podcast providers not supported yet + } + } } \ No newline at end of file diff --git a/client/strings/en-us.json b/client/strings/en-us.json index c9b2d687..2a68424e 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -104,7 +104,7 @@ "HeaderCollectionItems": "Collection Items", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Current Downloads", - "HeaderCustomMetadataProviders": "Custom metadata providers", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Queue", "HeaderEbookFiles": "Ebook Files", @@ -194,7 +194,6 @@ "LabelAllUsersExcludingGuests": "All users excluding guests", "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Already in your library", - "LabelApiKey": "API Key", "LabelAppend": "Append", "LabelAuthor": "Author", "LabelAuthorFirstLast": "Author (First Last)", @@ -536,7 +535,6 @@ "LabelUploaderDragAndDrop": "Drag & drop files or folders", "LabelUploaderDropFiles": "Drop files", "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", - "LabelUrl": "URL", "LabelUseChapterTrack": "Use chapter track", "LabelUseFullTrack": "Use full track", "LabelUser": "User", diff --git a/server/Database.js b/server/Database.js index d2bb6fa4..dd9a0550 100644 --- a/server/Database.js +++ b/server/Database.js @@ -700,45 +700,6 @@ class Database { }) } - /** - * Returns true if a custom provider with the given slug exists - * @param {string} providerSlug - * @return {boolean} - */ - async doesCustomProviderExistWithSlug(providerSlug) { - const id = providerSlug.split("custom-")[1] - - if (!id) { - return false - } - - return !!await this.customMetadataProviderModel.findByPk(id) - } - - /** - * Removes a custom metadata provider - * @param {string} id - */ - async removeCustomMetadataProviderById(id) { - // destroy metadta provider - await this.customMetadataProviderModel.destroy({ - where: { - id, - } - }) - - const slug = `custom-${id}`; - - // fallback libraries using it to google - await this.libraryModel.update({ - provider: "google", - }, { - where: { - provider: slug, - } - }); - } - /** * Clean invalid records in database * Series should have atleast one Book diff --git a/server/controllers/CustomMetadataProviderController.js b/server/controllers/CustomMetadataProviderController.js new file mode 100644 index 00000000..fdb4df2d --- /dev/null +++ b/server/controllers/CustomMetadataProviderController.js @@ -0,0 +1,117 @@ +const Logger = require('../Logger') +const SocketAuthority = require('../SocketAuthority') +const Database = require('../Database') + +const { validateUrl } = require('../utils/index') + +// +// This is a controller for routes that don't have a home yet :( +// +class CustomMetadataProviderController { + constructor() { } + + /** + * GET: /api/custom-metadata-providers + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async getAll(req, res) { + const providers = await Database.customMetadataProviderModel.findAll() + + res.json({ + providers + }) + } + + /** + * POST: /api/custom-metadata-providers + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async create(req, res) { + const { name, url, mediaType, authHeaderValue } = req.body + + if (!name || !url || !mediaType) { + return res.status(400).send('Invalid request body') + } + + const validUrl = validateUrl(url) + if (!validUrl) { + Logger.error(`[CustomMetadataProviderController] Invalid url "${url}"`) + return res.status(400).send('Invalid url') + } + + const provider = await Database.customMetadataProviderModel.create({ + name, + mediaType, + url, + authHeaderValue: !authHeaderValue ? null : authHeaderValue, + }) + + // TODO: Necessary to emit to all clients? + SocketAuthority.emitter('custom_metadata_provider_added', provider.toClientJson()) + + res.json({ + provider + }) + } + + /** + * DELETE: /api/custom-metadata-providers/:id + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async delete(req, res) { + const slug = `custom-${req.params.id}` + + /** @type {import('../models/CustomMetadataProvider')} */ + const provider = req.customMetadataProvider + const providerClientJson = provider.toClientJson() + + const fallbackProvider = provider.mediaType === 'book' ? 'google' : 'itunes' + + await provider.destroy() + + // Libraries using this provider fallback to default provider + await Database.libraryModel.update({ + provider: fallbackProvider + }, { + where: { + provider: slug + } + }) + + // TODO: Necessary to emit to all clients? + SocketAuthority.emitter('custom_metadata_provider_removed', providerClientJson) + + res.sendStatus(200) + } + + /** + * Middleware that requires admin or up + * + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next + */ + async middleware(req, res, next) { + if (!req.user.isAdminOrUp) { + Logger.warn(`[CustomMetadataProviderController] Non-admin user "${req.user.username}" attempted access route "${req.path}"`) + return res.sendStatus(403) + } + + // If id param then add req.customMetadataProvider + if (req.params.id) { + req.customMetadataProvider = await Database.customMetadataProviderModel.findByPk(req.params.id) + if (!req.customMetadataProvider) { + return res.sendStatus(404) + } + } + + next() + } +} +module.exports = new CustomMetadataProviderController() diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index b1ab572f..ecea310c 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -33,6 +33,14 @@ class LibraryController { return res.status(500).send('Invalid request') } + // Validate that the custom provider exists if given any + if (newLibraryPayload.provider?.startsWith('custom-')) { + if (!await Database.customMetadataProviderModel.checkExistsBySlug(newLibraryPayload.provider)) { + Logger.error(`[LibraryController] Custom metadata provider "${newLibraryPayload.provider}" does not exist`) + return res.status(400).send('Custom metadata provider does not exist') + } + } + // Validate folder paths exist or can be created & resolve rel paths // returns 400 if a folder fails to access newLibraryPayload.folders = newLibraryPayload.folders.map(f => { @@ -51,11 +59,6 @@ class LibraryController { } } - // Validate that the custom provider exists if given any - if (newLibraryPayload.provider && newLibraryPayload.provider.startsWith("custom-")) { - await Database.doesCustomProviderExistWithSlug(newLibraryPayload.provider) - } - const library = new Library() let currentLargestDisplayOrder = await Database.libraryModel.getMaxDisplayOrder() @@ -91,19 +94,27 @@ class LibraryController { }) } + /** + * GET: /api/libraries/:id + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ async findOne(req, res) { const includeArray = (req.query.include || '').split(',') if (includeArray.includes('filterdata')) { const filterdata = await libraryFilters.getFilterData(req.library.mediaType, req.library.id) + const customMetadataProviders = await Database.customMetadataProviderModel.getForClientByMediaType(req.library.mediaType) return res.json({ filterdata, issues: filterdata.numIssues, numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id), + customMetadataProviders, library: req.library }) } - return res.json(req.library) + res.json(req.library) } /** @@ -120,6 +131,14 @@ class LibraryController { async update(req, res) { const library = req.library + // Validate that the custom provider exists if given any + if (req.body.provider?.startsWith('custom-')) { + if (!await Database.customMetadataProviderModel.checkExistsBySlug(req.body.provider)) { + Logger.error(`[LibraryController] Custom metadata provider "${req.body.provider}" does not exist`) + return res.status(400).send('Custom metadata provider does not exist') + } + } + // Validate new folder paths exist or can be created & resolve rel paths // returns 400 if a new folder fails to access if (req.body.folders) { @@ -180,11 +199,6 @@ class LibraryController { } } - // Validate that the custom provider exists if given any - if (req.body.provider && req.body.provider.startsWith("custom-")) { - await Database.doesCustomProviderExistWithSlug(req.body.provider) - } - const hasUpdates = library.update(req.body) // TODO: Should check if this is an update to folder paths or name only if (hasUpdates) { diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 1d2fff04..c2272ee6 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -717,95 +717,5 @@ class MiscController { const stats = await adminStats.getStatsForYear(year) res.json(stats) } - - /** - * GET: /api/custom-metadata-providers - * - * @param {import('express').Request} req - * @param {import('express').Response} res - */ - async getCustomMetadataProviders(req, res) { - const providers = await Database.customMetadataProviderModel.findAll() - - res.json({ - providers: providers.map((p) => p.toUserJson()), - }) - } - - /** - * GET: /api/custom-metadata-providers/admin - * - * @param {import('express').Request} req - * @param {import('express').Response} res - */ - async getAdminCustomMetadataProviders(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get admin custom metadata providers`) - return res.sendStatus(403) - } - - const providers = await Database.customMetadataProviderModel.findAll() - - res.json({ - providers, - }) - } - - /** - * PATCH: /api/custom-metadata-providers/admin - * - * @param {import('express').Request} req - * @param {import('express').Response} res - */ - async addCustomMetadataProviders(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to add admin custom metadata providers`) - return res.sendStatus(403) - } - - const { name, url, apiKey } = req.body - - if (!name || !url || !apiKey) { - return res.status(500).send(`Invalid patch data`) - } - - const provider = await Database.customMetadataProviderModel.create({ - name, - url, - apiKey, - }) - - SocketAuthority.adminEmitter('custom_metadata_provider_added', provider) - - res.json({ - provider, - }) - } - - /** - * DELETE: /api/custom-metadata-providers/admin/:id - * - * @param {import('express').Request} req - * @param {import('express').Response} res - */ - async deleteCustomMetadataProviders(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to delete admin custom metadata providers`) - return res.sendStatus(403) - } - - const { id } = req.params - - if (!id) { - return res.status(500).send(`Invalid delete data`) - } - - const provider = await Database.customMetadataProviderModel.findByPk(id) - await Database.removeCustomMetadataProviderById(id) - - SocketAuthority.adminEmitter('custom_metadata_provider_removed', provider) - - res.sendStatus(200) - } } module.exports = new MiscController() diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 22fcaa1c..7626bd12 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -161,7 +161,7 @@ class SessionController { * @typedef batchDeleteReqBody * @property {string[]} sessions * - * @param {import('express').Request<{}, {}, batchDeleteReqBody, {}} req + * @param {import('express').Request<{}, {}, batchDeleteReqBody, {}>} req * @param {import('express').Response} res */ async batchDelete(req, res) { diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 6c35a5fb..7ba97ed1 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -149,6 +149,13 @@ class BookFinder { return books } + /** + * + * @param {string} title + * @param {string} author + * @param {string} providerSlug + * @returns {Promise<Object[]>} + */ async getCustomProviderResults(title, author, providerSlug) { const books = await this.customProviderAdapter.search(title, author, providerSlug) if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`) @@ -325,8 +332,8 @@ class BookFinder { let numFuzzySearches = 0 // Custom providers are assumed to be correct - if (provider.startsWith("custom-")) { - return await this.getCustomProviderResults(title, author, provider) + if (provider.startsWith('custom-')) { + return this.getCustomProviderResults(title, author, provider) } if (!title) diff --git a/server/models/CustomMetadataProvider.js b/server/models/CustomMetadataProvider.js index d6047bb8..8218e419 100644 --- a/server/models/CustomMetadataProvider.js +++ b/server/models/CustomMetadataProvider.js @@ -1,4 +1,12 @@ -const { DataTypes, Model, Sequelize } = require('sequelize') +const { DataTypes, Model } = require('sequelize') + +/** + * @typedef ClientCustomMetadataProvider + * @property {UUIDV4} id + * @property {string} name + * @property {string} url + * @property {string} slug + */ class CustomMetadataProvider extends Model { constructor(values, options) { @@ -7,26 +15,67 @@ class CustomMetadataProvider extends Model { /** @type {UUIDV4} */ this.id /** @type {string} */ + this.mediaType + /** @type {string} */ this.name /** @type {string} */ this.url /** @type {string} */ - this.apiKey + this.authHeaderValue + /** @type {Object} */ + this.extraData + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt } getSlug() { return `custom-${this.id}` } - toUserJson() { + /** + * Safe for clients + * @returns {ClientCustomMetadataProvider} + */ + toClientJson() { return { - name: this.name, id: this.id, + name: this.name, + mediaType: this.mediaType, slug: this.getSlug() } } + /** + * Get providers for client by media type + * Currently only available for "book" media type + * + * @param {string} mediaType + * @returns {Promise<ClientCustomMetadataProvider[]>} + */ + static async getForClientByMediaType(mediaType) { + if (mediaType !== 'book') return [] + const customMetadataProviders = await this.findAll({ + where: { + mediaType + } + }) + return customMetadataProviders.map(cmp => cmp.toClientJson()) + } + /** + * Check if provider exists by slug + * + * @param {string} providerSlug + * @returns {Promise<boolean>} + */ + static async checkExistsBySlug(providerSlug) { + const providerId = providerSlug?.split?.('custom-')[1] + if (!providerId) return false + + return (await this.count({ where: { id: providerId } })) > 0 + } /** * Initialize model @@ -40,8 +89,10 @@ class CustomMetadataProvider extends Model { primaryKey: true }, name: DataTypes.STRING, + mediaType: DataTypes.STRING, url: DataTypes.STRING, - apiKey: DataTypes.STRING, + authHeaderValue: DataTypes.STRING, + extraData: DataTypes.JSON }, { sequelize, modelName: 'customMetadataProvider' diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js index 1919ecc9..36f4c930 100644 --- a/server/providers/CustomProviderAdapter.js +++ b/server/providers/CustomProviderAdapter.js @@ -1,32 +1,40 @@ const Database = require('../Database') -const axios = require("axios") -const Logger = require("../Logger") +const axios = require('axios') +const Logger = require('../Logger') class CustomProviderAdapter { - constructor() { - } + constructor() { } + /** + * + * @param {string} title + * @param {string} author + * @param {string} providerSlug + * @returns {Promise<Object[]>} + */ async search(title, author, providerSlug) { - const providerId = providerSlug.split("custom-")[1] + const providerId = providerSlug.split('custom-')[1] const provider = await Database.customMetadataProviderModel.findByPk(providerId) if (!provider) { throw new Error("Custom provider not found for the given id") } - const matches = await axios.get(`${provider.url}/search?query=${encodeURIComponent(title)}${!!author ? `&author=${encodeURIComponent(author)}` : ""}`, { - headers: { - "Authorization": provider.apiKey, - }, - }).then((res) => { - if (!res || !res.data || !Array.isArray(res.data.matches)) return null + const axiosOptions = {} + if (provider.authHeaderValue) { + axiosOptions.headers = { + 'Authorization': provider.authHeaderValue + } + } + const matches = await axios.get(`${provider.url}/search?query=${encodeURIComponent(title)}${!!author ? `&author=${encodeURIComponent(author)}` : ""}`, axiosOptions).then((res) => { + if (!res?.data || !Array.isArray(res.data.matches)) return null return res.data.matches }).catch(error => { Logger.error('[CustomMetadataProvider] Search error', error) return [] }) - if (matches === null) { + if (!matches) { throw new Error("Custom provider returned malformed response") } @@ -46,7 +54,7 @@ class CustomProviderAdapter { tags, series, language, - duration, + duration }) => { return { title, @@ -60,10 +68,10 @@ class CustomProviderAdapter { isbn, asin, genres, - tags: tags.join(","), - series: series.length ? series : null, + tags: tags?.join(',') || null, + series: series?.length ? series : null, language, - duration, + duration } }) } diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 99769648..a2688b88 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -28,6 +28,7 @@ const SearchController = require('../controllers/SearchController') const CacheController = require('../controllers/CacheController') const ToolsController = require('../controllers/ToolsController') const RSSFeedController = require('../controllers/RSSFeedController') +const CustomMetadataProviderController = require('../controllers/CustomMetadataProviderController') const MiscController = require('../controllers/MiscController') const Author = require('../objects/entities/Author') @@ -299,6 +300,14 @@ class ApiRouter { this.router.post('/feeds/series/:seriesId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForSeries.bind(this)) this.router.post('/feeds/:id/close', RSSFeedController.middleware.bind(this), RSSFeedController.closeRSSFeed.bind(this)) + // + // Custom Metadata Provider routes + // + this.router.get('/custom-metadata-providers', CustomMetadataProviderController.middleware.bind(this), CustomMetadataProviderController.getAll.bind(this)) + this.router.post('/custom-metadata-providers', CustomMetadataProviderController.middleware.bind(this), CustomMetadataProviderController.create.bind(this)) + this.router.delete('/custom-metadata-providers/:id', CustomMetadataProviderController.middleware.bind(this), CustomMetadataProviderController.delete.bind(this)) + + // // Misc Routes // @@ -318,10 +327,6 @@ class ApiRouter { this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this)) this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this)) this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this)) - this.router.get('/custom-metadata-providers', MiscController.getCustomMetadataProviders.bind(this)) - this.router.get('/custom-metadata-providers/admin', MiscController.getAdminCustomMetadataProviders.bind(this)) - this.router.patch('/custom-metadata-providers/admin', MiscController.addCustomMetadataProviders.bind(this)) - this.router.delete('/custom-metadata-providers/admin/:id', MiscController.deleteCustomMetadataProviders.bind(this)) } // From 4e3e7b10ce70042d2ffa9e391cb18af6de9f8377 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 12 Feb 2024 17:12:49 -0600 Subject: [PATCH 0355/2145] Update:Custom metadata provider adapter sends mediaType as a query param --- server/finders/BookFinder.js | 2 +- server/providers/CustomProviderAdapter.js | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 7ba97ed1..f218587c 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -157,7 +157,7 @@ class BookFinder { * @returns {Promise<Object[]>} */ async getCustomProviderResults(title, author, providerSlug) { - const books = await this.customProviderAdapter.search(title, author, providerSlug) + const books = await this.customProviderAdapter.search(title, author, providerSlug, 'book') if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`) return books diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js index 36f4c930..3d5209c7 100644 --- a/server/providers/CustomProviderAdapter.js +++ b/server/providers/CustomProviderAdapter.js @@ -10,9 +10,10 @@ class CustomProviderAdapter { * @param {string} title * @param {string} author * @param {string} providerSlug + * @param {string} mediaType * @returns {Promise<Object[]>} */ - async search(title, author, providerSlug) { + async search(title, author, providerSlug, mediaType) { const providerId = providerSlug.split('custom-')[1] const provider = await Database.customMetadataProviderModel.findByPk(providerId) @@ -20,13 +21,25 @@ class CustomProviderAdapter { throw new Error("Custom provider not found for the given id") } + // Setup query params + const queryObj = { + mediaType, + query: title + } + if (author) { + queryObj.author = author + } + const queryString = (new URLSearchParams(queryObj)).toString() + + // Setup headers const axiosOptions = {} if (provider.authHeaderValue) { axiosOptions.headers = { 'Authorization': provider.authHeaderValue } } - const matches = await axios.get(`${provider.url}/search?query=${encodeURIComponent(title)}${!!author ? `&author=${encodeURIComponent(author)}` : ""}`, axiosOptions).then((res) => { + + const matches = await axios.get(`${provider.url}/search?${queryString}}`, axiosOptions).then((res) => { if (!res?.data || !Array.isArray(res.data.matches)) return null return res.data.matches }).catch(error => { From 6111e8f0dadfe0750cb9fb12d7ae7b1a34dfb966 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 13 Feb 2024 18:45:01 -0600 Subject: [PATCH 0356/2145] Fix:Global search menu for mobile --- client/components/controls/GlobalSearch.vue | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/client/components/controls/GlobalSearch.vue b/client/components/controls/GlobalSearch.vue index 0c5ba41b..9716ba49 100644 --- a/client/components/controls/GlobalSearch.vue +++ b/client/components/controls/GlobalSearch.vue @@ -1,13 +1,15 @@ <template> - <div class="sm:w-80 w-full relative"> - <form @submit.prevent="submitSearch"> - <ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" /> - </form> - <div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear"> - <span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span> - <span v-else class="material-icons" style="font-size: 1.2rem">close</span> + <div class=""> + <div class="w-full relative sm:w-80"> + <form @submit.prevent="submitSearch"> + <ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" /> + </form> + <div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear"> + <span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span> + <span v-else class="material-icons" style="font-size: 1.2rem">close</span> + </div> </div> - <div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu"> + <div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu"> <ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <li v-if="isTyping" class="py-2 px-2"> <p>{{ $strings.MessageThinking }}</p> From 44135b3fedf1a62d7dbe2273b122267f4f163714 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 14 Feb 2024 18:12:35 -0600 Subject: [PATCH 0357/2145] Rename StreamContainer to MediaPlayerContainer --- ...eamContainer.vue => MediaPlayerContainer.vue} | 14 +++++++------- client/layouts/default.vue | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) rename client/components/app/{StreamContainer.vue => MediaPlayerContainer.vue} (96%) diff --git a/client/components/app/StreamContainer.vue b/client/components/app/MediaPlayerContainer.vue similarity index 96% rename from client/components/app/StreamContainer.vue rename to client/components/app/MediaPlayerContainer.vue index 0cae1421..2e0afc55 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/MediaPlayerContainer.vue @@ -1,5 +1,5 @@ <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 v-if="streamLibraryItem" id="mediaPlayerContainer" 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" /> <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" /> @@ -380,7 +380,7 @@ export default { if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) { if (!data.numSegments) return var chunks = data.chunks - console.log(`[StreamContainer] Stream Progress ${data.percent}`) + console.log(`[MediaPlayerContainer] Stream Progress ${data.percent}`) if (this.$refs.audioPlayer) { this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments) } else { @@ -397,17 +397,17 @@ export default { this.playerHandler.prepareOpenSession(session, this.currentPlaybackRate) }, streamOpen(session) { - console.log(`[StreamContainer] Stream session open`, session) + console.log(`[MediaPlayerContainer] Stream session open`, session) }, streamClosed(streamId) { // Stream was closed from the server if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) { - console.warn('[StreamContainer] Closing stream due to request from server') + console.warn('[MediaPlayerContainer] Closing stream due to request from server') this.playerHandler.closePlayer() } }, streamReady() { - console.log(`[StreamContainer] Stream Ready`) + console.log(`[MediaPlayerContainer] Stream Ready`) if (this.$refs.audioPlayer) { this.$refs.audioPlayer.setStreamReady() } else { @@ -417,7 +417,7 @@ export default { streamError(streamId) { // Stream had critical error from the server if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) { - console.warn('[StreamContainer] Closing stream due to stream error from server') + console.warn('[MediaPlayerContainer] Closing stream due to stream error from server') this.playerHandler.closePlayer() } }, @@ -496,7 +496,7 @@ export default { </script> <style> -#streamContainer { +#mediaPlayerContainer { box-shadow: 0px -6px 8px #1111113f; } </style> diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 41d4943a..6d5e0230 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -7,7 +7,7 @@ <Nuxt :key="currentLang" /> </div> - <app-stream-container ref="streamContainer" /> + <app-media-player-container ref="mediaPlayerContainer" /> <modals-item-edit-modal /> <modals-collections-add-create-modal /> @@ -129,23 +129,23 @@ export default { this.$eventBus.$emit('socket_init') }, streamOpen(stream) { - if (this.$refs.streamContainer) this.$refs.streamContainer.streamOpen(stream) + if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream) }, streamClosed(streamId) { - if (this.$refs.streamContainer) this.$refs.streamContainer.streamClosed(streamId) + if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamClosed(streamId) }, streamProgress(data) { - if (this.$refs.streamContainer) this.$refs.streamContainer.streamProgress(data) + if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamProgress(data) }, streamReady() { - if (this.$refs.streamContainer) this.$refs.streamContainer.streamReady() + if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamReady() }, streamReset(payload) { - if (this.$refs.streamContainer) this.$refs.streamContainer.streamReset(payload) + if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamReset(payload) }, streamError({ id, errorMessage }) { this.$toast.error(`Stream Failed: ${errorMessage}`) - if (this.$refs.streamContainer) this.$refs.streamContainer.streamError(id) + if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamError(id) }, libraryAdded(library) { this.$store.commit('libraries/addUpdate', library) @@ -247,7 +247,7 @@ export default { this.multiSessionCurrentSessionId = null this.$toast.dismiss('multiple-sessions') } - if (this.$refs.streamContainer) this.$refs.streamContainer.sessionClosedEvent(sessionId) + if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.sessionClosedEvent(sessionId) }, userMediaProgressUpdate(payload) { this.$store.commit('user/updateMediaProgress', payload) From 04d16fc5353285e2878954afcd206676886a8269 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 14 Feb 2024 18:28:19 -0600 Subject: [PATCH 0358/2145] Fix:Audio player buttons to use button el and add aria-label translations #2599 --- .../components/app/MediaPlayerContainer.vue | 2 +- client/components/controls/VolumeControl.vue | 10 +++++----- .../player/PlayerPlaybackControls.vue | 20 +++++++++---------- client/components/player/PlayerUi.vue | 18 ++++++++--------- client/strings/cs.json | 6 ++++++ client/strings/da.json | 6 ++++++ client/strings/de.json | 8 +++++++- client/strings/en-us.json | 5 +++++ client/strings/es.json | 6 ++++++ 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 | 6 ++++++ client/strings/no.json | 6 ++++++ client/strings/pl.json | 6 ++++++ client/strings/ru.json | 6 ++++++ client/strings/sv.json | 6 ++++++ client/strings/zh-cn.json | 6 ++++++ 21 files changed, 127 insertions(+), 26 deletions(-) diff --git a/client/components/app/MediaPlayerContainer.vue b/client/components/app/MediaPlayerContainer.vue index 2e0afc55..120231dc 100644 --- a/client/components/app/MediaPlayerContainer.vue +++ b/client/components/app/MediaPlayerContainer.vue @@ -29,7 +29,7 @@ </div> <div class="flex-grow" /> <ui-tooltip direction="top" :text="$strings.LabelClosePlayer"> - <span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span> + <button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button> </ui-tooltip> </div> <player-ui diff --git a/client/components/controls/VolumeControl.vue b/client/components/controls/VolumeControl.vue index 7212357b..f9734f6f 100644 --- a/client/components/controls/VolumeControl.vue +++ b/client/components/controls/VolumeControl.vue @@ -1,8 +1,8 @@ <template> <div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave"> - <div class="cursor-pointer text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon"> + <button :aria-label="$strings.LabelVolume" class="text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon"> <span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span> - </div> + </button> <transition name="menux"> <div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px"> <div ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack"> @@ -38,8 +38,8 @@ export default { }, set(val) { try { - localStorage.setItem("volume", val); - } catch(error) { + localStorage.setItem('volume', val) + } catch (error) { console.error('Failed to store volume', err) } this.$emit('input', val) @@ -146,7 +146,7 @@ export default { if (this.value === 0) { this.isMute = true } - const storageVolume = localStorage.getItem("volume") + const storageVolume = localStorage.getItem('volume') if (storageVolume) { this.volume = parseFloat(storageVolume) } diff --git a/client/components/player/PlayerPlaybackControls.vue b/client/components/player/PlayerPlaybackControls.vue index a66e40d6..91bc3be3 100644 --- a/client/components/player/PlayerPlaybackControls.vue +++ b/client/components/player/PlayerPlaybackControls.vue @@ -2,21 +2,21 @@ <div class="flex pt-4 pb-2 md:pt-0 md:pb-2"> <div class="flex-grow" /> <template v-if="!loading"> - <div class="cursor-pointer flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter"> + <button :aria-label="$strings.ButtonPreviousChapter" class="flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter"> <span class="material-icons text-2xl sm:text-3xl">first_page</span> - </div> - <div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward"> + </button> + <button :aria-label="$strings.ButtonJumpBackward" class="flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward"> <span class="material-icons text-2xl sm:text-3xl">replay_10</span> - </div> - <div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause"> + </button> + <button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause"> <span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span> - </div> - <div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward"> + </button> + <button :aria-label="$strings.ButtonJumpForward" class="flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward"> <span class="material-icons text-2xl sm:text-3xl">forward_10</span> - </div> - <div class="flex items-center justify-center ml-4 md:ml-8" :class="hasNextChapter ? 'text-gray-300 cursor-pointer' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter"> + </button> + <button :aria-label="$strings.ButtonNextChapter" class="flex items-center justify-center ml-4 md:ml-8" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter"> <span class="material-icons text-2xl sm:text-3xl">last_page</span> - </div> + </button> <controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" /> </template> <template v-else> diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue index e1b0f96d..8d26dd99 100644 --- a/client/components/player/PlayerUi.vue +++ b/client/components/player/PlayerUi.vue @@ -9,37 +9,37 @@ </ui-tooltip> <ui-tooltip direction="top" :text="$strings.LabelSleepTimer"> - <div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')"> + <button :aria-label="$strings.LabelSleepTimer" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')"> <span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span> <div v-else class="flex items-center"> <span class="material-icons text-lg text-warning">snooze</span> <p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p> </div> - </div> + </button> </ui-tooltip> <ui-tooltip v-if="!isPodcast" direction="top" :text="$strings.LabelViewBookmarks"> - <div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')"> + <button :aria-label="$strings.LabelViewBookmarks" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')"> <span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span> - </div> + </button> </ui-tooltip> <ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters"> - <div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters"> + <button :aria-label="$strings.LabelViewChapters" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters"> <span class="material-icons text-2xl">format_list_bulleted</span> - </div> + </button> </ui-tooltip> <ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue"> - <button class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')"> + <button :aria-label="$strings.LabelViewQueue" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')"> <span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span> </button> </ui-tooltip> <ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack"> - <div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack"> + <button :aria-label="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack" class="text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack"> <span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span> - </div> + </button> </ui-tooltip> </div> diff --git a/client/strings/cs.json b/client/strings/cs.json index 1e761882..d7afc3e9 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -32,6 +32,8 @@ "ButtonHide": "Skrýt", "ButtonHome": "Domů", "ButtonIssues": "Problémy", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Nejnovější", "ButtonLibrary": "Knihovna", "ButtonLogout": "Odhlásit", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Spárovat všechny autory", "ButtonMatchBooks": "Spárovat Knihy", "ButtonNevermind": "Nevadí", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Otevřít kanál", "ButtonOpenManager": "Otevřít správce", + "ButtonPause": "Pause", "ButtonPlay": "Přehrát", "ButtonPlaying": "Hraje", "ButtonPlaylists": "Seznamy skladeb", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť", "ButtonPurgeItemsCache": "Vyčistit mezipaměť položek", "ButtonPurgeMediaProgress": "Vyčistit průběh médií", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Položky kolekce", "HeaderCover": "Obálka", "HeaderCurrentDownloads": "Aktuální stahování", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Podrobnosti", "HeaderDownloadQueue": "Fronta stahování", "HeaderEbookFiles": "Soubory elektronických knih", diff --git a/client/strings/da.json b/client/strings/da.json index 07d7d5a3..5b6d826e 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -32,6 +32,8 @@ "ButtonHide": "Skjul", "ButtonHome": "Hjem", "ButtonIssues": "Problemer", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Seneste", "ButtonLibrary": "Bibliotek", "ButtonLogout": "Log ud", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Match alle forfattere", "ButtonMatchBooks": "Match bøger", "ButtonNevermind": "Glem det", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "OK", "ButtonOpenFeed": "Åbn feed", "ButtonOpenManager": "Åbn manager", + "ButtonPause": "Pause", "ButtonPlay": "Afspil", "ButtonPlaying": "Afspiller", "ButtonPlaylists": "Afspilningslister", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Ryd al cache", "ButtonPurgeItemsCache": "Ryd elementcache", "ButtonPurgeMediaProgress": "Ryd Medieforløb", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Samlingselementer", "HeaderCover": "Omslag", "HeaderCurrentDownloads": "Nuværende Downloads", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Detaljer", "HeaderDownloadQueue": "Download Kø", "HeaderEbookFiles": "E-bogsfiler", diff --git a/client/strings/de.json b/client/strings/de.json index fe5b3e02..47a26467 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -32,6 +32,8 @@ "ButtonHide": "Ausblenden", "ButtonHome": "Startseite", "ButtonIssues": "Probleme", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Neuste", "ButtonLibrary": "Bibliothek", "ButtonLogout": "Abmelden", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)", "ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)", "ButtonNevermind": "Abbrechen", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Feed öffnen", "ButtonOpenManager": "Manager öffnen", + "ButtonPause": "Pause", "ButtonPlay": "Abspielen", "ButtonPlaying": "Spielt", "ButtonPlaylists": "Wiedergabelisten", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Cache leeren", "ButtonPurgeItemsCache": "Lösche Medien-Cache", "ButtonPurgeMediaProgress": "Lösche Hörfortschritte", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Sammlungseinträge", "HeaderCover": "Titelbild", "HeaderCurrentDownloads": "Aktuelle Downloads", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Warteschlange", "HeaderEbookFiles": "E-Book Dateien", @@ -758,4 +764,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 2a68424e..eb30c519 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -32,6 +32,8 @@ "ButtonHide": "Hide", "ButtonHome": "Home", "ButtonIssues": "Issues", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Latest", "ButtonLibrary": "Library", "ButtonLogout": "Logout", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Match All Authors", "ButtonMatchBooks": "Match Books", "ButtonNevermind": "Nevermind", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Open Feed", "ButtonOpenManager": "Open Manager", + "ButtonPause": "Pause", "ButtonPlay": "Play", "ButtonPlaying": "Playing", "ButtonPlaylists": "Playlists", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Purge All Cache", "ButtonPurgeItemsCache": "Purge Items Cache", "ButtonPurgeMediaProgress": "Purge Media Progress", diff --git a/client/strings/es.json b/client/strings/es.json index 24496683..56876087 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -32,6 +32,8 @@ "ButtonHide": "Esconder", "ButtonHome": "Inicio", "ButtonIssues": "Problemas", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Últimos", "ButtonLibrary": "Biblioteca", "ButtonLogout": "Cerrar Sesión", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Encontrar Todos los Autores", "ButtonMatchBooks": "Encontrar Libros", "ButtonNevermind": "Olvidar", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Abrir Fuente", "ButtonOpenManager": "Abrir Editor", + "ButtonPause": "Pause", "ButtonPlay": "Reproducir", "ButtonPlaying": "Reproduciendo", "ButtonPlaylists": "Listas de Reproducción", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Purgar Todo el Cache", "ButtonPurgeItemsCache": "Purgar Elementos de Cache", "ButtonPurgeMediaProgress": "Purgar Progreso de Multimedia", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Elementos en la Colección", "HeaderCover": "Portada", "HeaderCurrentDownloads": "Descargando Actualmente", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Detalles", "HeaderDownloadQueue": "Lista de Descarga", "HeaderEbookFiles": "Archivos de Ebook", diff --git a/client/strings/fr.json b/client/strings/fr.json index 2fe6f6e4..7e1f358c 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -32,6 +32,8 @@ "ButtonHide": "Cacher", "ButtonHome": "Accueil", "ButtonIssues": "Parutions", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Dernière version", "ButtonLibrary": "Bibliothèque", "ButtonLogout": "Me déconnecter", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Chercher tous les auteurs", "ButtonMatchBooks": "Chercher les livres", "ButtonNevermind": "Non merci", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Ouvrir le flux", "ButtonOpenManager": "Ouvrir le gestionnaire", + "ButtonPause": "Pause", "ButtonPlay": "Écouter", "ButtonPlaying": "En lecture", "ButtonPlaylists": "Listes de lecture", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Purger le cache", "ButtonPurgeItemsCache": "Purger le cache des articles", "ButtonPurgeMediaProgress": "Purger la progression des médias", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Entrées de la collection", "HeaderCover": "Couverture", "HeaderCurrentDownloads": "Téléchargements en cours", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Détails", "HeaderDownloadQueue": "File d’attente de téléchargements", "HeaderEbookFiles": "Fichier des livres numériques", diff --git a/client/strings/gu.json b/client/strings/gu.json index 349ff8e5..6f3cdbb9 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -32,6 +32,8 @@ "ButtonHide": "છુપાવો", "ButtonHome": "ઘર", "ButtonIssues": "સમસ્યાઓ", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "નવીનતમ", "ButtonLibrary": "પુસ્તકાલય", "ButtonLogout": "લૉગ આઉટ", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "બધા મેળ ખાતા લેખકો શોધો", "ButtonMatchBooks": "મેળ ખાતી પુસ્તકો શોધો", "ButtonNevermind": "કંઈ વાંધો નહીં", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "ઓકે", "ButtonOpenFeed": "ફીડ ખોલો", "ButtonOpenManager": "મેનેજર ખોલો", + "ButtonPause": "Pause", "ButtonPlay": "ચલાવો", "ButtonPlaying": "ચલાવી રહ્યું છે", "ButtonPlaylists": "પ્લેલિસ્ટ", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "બધો Cache કાઢી નાખો", "ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો", "ButtonPurgeMediaProgress": "બધું સાંભળ્યું કાઢી નાખો", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "સંગ્રહ વસ્તુઓ", "HeaderCover": "આવરણ", "HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "વિગતો", "HeaderDownloadQueue": "ડાઉનલોડ કતાર", "HeaderEbookFiles": "ઇબુક ફાઇલો", diff --git a/client/strings/hi.json b/client/strings/hi.json index fcc174d9..25b8e7a5 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -32,6 +32,8 @@ "ButtonHide": "छुपाएं", "ButtonHome": "घर", "ButtonIssues": "समस्याएं", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "नवीनतम", "ButtonLibrary": "पुस्तकालय", "ButtonLogout": "लॉग आउट", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "सभी लेखकों को तलाश करें", "ButtonMatchBooks": "संबंधित पुस्तकों का मिलान करें", "ButtonNevermind": "कोई बात नहीं", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "ठीक है", "ButtonOpenFeed": "फ़ीड खोलें", "ButtonOpenManager": "मैनेजर खोलें", + "ButtonPause": "Pause", "ButtonPlay": "चलाएँ", "ButtonPlaying": "चल रही है", "ButtonPlaylists": "प्लेलिस्ट्स", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "सभी Cache मिटाएं", "ButtonPurgeItemsCache": "आइटम Cache मिटाएं", "ButtonPurgeMediaProgress": "अभी तक सुना हुआ सब हटा दे", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Collection Items", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Current Downloads", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Queue", "HeaderEbookFiles": "Ebook Files", diff --git a/client/strings/hr.json b/client/strings/hr.json index 0aa37771..f15d2065 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -32,6 +32,8 @@ "ButtonHide": "Sakrij", "ButtonHome": "Početna stranica", "ButtonIssues": "Problemi", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Najnovije", "ButtonLibrary": "Biblioteka", "ButtonLogout": "Odjavi se", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Matchaj sve autore", "ButtonMatchBooks": "Matchaj knjige", "ButtonNevermind": "Nije bitno", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Otvori feed", "ButtonOpenManager": "Otvori menadžera", + "ButtonPause": "Pause", "ButtonPlay": "Pokreni", "ButtonPlaying": "Playing", "ButtonPlaylists": "Playlists", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Isprazni sav cache", "ButtonPurgeItemsCache": "Isprazni Items Cache", "ButtonPurgeMediaProgress": "Purge Media Progress", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Stvari u kolekciji", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Current Downloads", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Detalji", "HeaderDownloadQueue": "Download Queue", "HeaderEbookFiles": "Ebook Files", diff --git a/client/strings/it.json b/client/strings/it.json index 077a225d..be12bb51 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -32,6 +32,8 @@ "ButtonHide": "Nascondi", "ButtonHome": "Home", "ButtonIssues": "Errori", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Ultimi", "ButtonLibrary": "Libreria", "ButtonLogout": "Disconnetti", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Aggiungi metadata agli Autori", "ButtonMatchBooks": "Aggiungi metadata della Libreria", "ButtonNevermind": "Nevermind", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Apri Feed", "ButtonOpenManager": "Apri Manager", + "ButtonPause": "Pause", "ButtonPlay": "Play", "ButtonPlaying": "In Riproduzione", "ButtonPlaylists": "Playlists", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Elimina tutta la Cache", "ButtonPurgeItemsCache": "Elimina la Cache selezionata", "ButtonPurgeMediaProgress": "Elimina info dei media ascoltati", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Elementi della Raccolta", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Download Correnti", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Dettagli", "HeaderDownloadQueue": "Download Queue", "HeaderEbookFiles": "Ebook File", diff --git a/client/strings/lt.json b/client/strings/lt.json index 43cfb8ee..add5c575 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -32,6 +32,8 @@ "ButtonHide": "Slėpti", "ButtonHome": "Pradžia", "ButtonIssues": "Problemos", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Naujausias", "ButtonLibrary": "Biblioteka", "ButtonLogout": "Atsijungti", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Pritaikyti visus autorius", "ButtonMatchBooks": "Pritaikyti knygas", "ButtonNevermind": "Nesvarbu", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Atidaryti srautą", "ButtonOpenManager": "Atidaryti tvarkyklę", + "ButtonPause": "Pause", "ButtonPlay": "Groti", "ButtonPlaying": "Grojama", "ButtonPlaylists": "Grojaraščiai", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Valyti visą saugyklą", "ButtonPurgeItemsCache": "Valyti elementų saugyklą", "ButtonPurgeMediaProgress": "Valyti medijos progresą", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Kolekcijos elementai", "HeaderCover": "Viršelis", "HeaderCurrentDownloads": "Dabartiniai parsisiuntimai", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Detalės", "HeaderDownloadQueue": "Parsisiuntimo eilė", "HeaderEbookFiles": "Eknygos failai", diff --git a/client/strings/nl.json b/client/strings/nl.json index bffe649d..bc4974e3 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -32,6 +32,8 @@ "ButtonHide": "Verberg", "ButtonHome": "Home", "ButtonIssues": "Issues", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Meest recent", "ButtonLibrary": "Bibliotheek", "ButtonLogout": "Log uit", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Alle auteurs matchen", "ButtonMatchBooks": "Alle boeken matchen", "ButtonNevermind": "Laat maar", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Feed openen", "ButtonOpenManager": "Manager openen", + "ButtonPause": "Pause", "ButtonPlay": "Afspelen", "ButtonPlaying": "Speelt", "ButtonPlaylists": "Afspeellijsten", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Volledige cache legen", "ButtonPurgeItemsCache": "Onderdelen-cache legen", "ButtonPurgeMediaProgress": "Mediavoortgang legen", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Collectie-objecten", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Huidige downloads", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download-wachtrij", "HeaderEbookFiles": "Ebook Files", diff --git a/client/strings/no.json b/client/strings/no.json index 109b2200..72d3eed4 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -32,6 +32,8 @@ "ButtonHide": "Gjøm", "ButtonHome": "Hjem", "ButtonIssues": "Problemer", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Siste", "ButtonLibrary": "Bibliotek", "ButtonLogout": "Logg ut", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Søk opp alle forfattere", "ButtonMatchBooks": "Søk opp bøker", "ButtonNevermind": "Avbryt", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Åpne Feed", "ButtonOpenManager": "Åpne behandler", + "ButtonPause": "Pause", "ButtonPlay": "Spill av", "ButtonPlaying": "Spiller av", "ButtonPlaylists": "Spilleliste", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Tøm alle mellomlager", "ButtonPurgeItemsCache": "Tøm mellomlager", "ButtonPurgeMediaProgress": "Slett medie fremgang", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Samlingsgjenstander", "HeaderCover": "Omslag", "HeaderCurrentDownloads": "Aktive nedlastinger", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Detaljer", "HeaderDownloadQueue": "Last ned kø", "HeaderEbookFiles": "Ebook filer", diff --git a/client/strings/pl.json b/client/strings/pl.json index c304f187..8db18059 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -32,6 +32,8 @@ "ButtonHide": "Ukryj", "ButtonHome": "Strona główna", "ButtonIssues": "Błędy", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Aktualna wersja:", "ButtonLibrary": "Biblioteka", "ButtonLogout": "Wyloguj", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Dopasuj wszystkich autorów", "ButtonMatchBooks": "Dopasuj książki", "ButtonNevermind": "Anuluj", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Otwórz feed", "ButtonOpenManager": "Otwórz menadżera", + "ButtonPause": "Pause", "ButtonPlay": "Odtwarzaj", "ButtonPlaying": "Odtwarzane", "ButtonPlaylists": "Playlists", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Wyczyść dane tymczasowe", "ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji", "ButtonPurgeMediaProgress": "Wyczyść postęp", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Elementy kolekcji", "HeaderCover": "Okładka", "HeaderCurrentDownloads": "Current Downloads", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Szczegóły", "HeaderDownloadQueue": "Download Queue", "HeaderEbookFiles": "Ebook Files", diff --git a/client/strings/ru.json b/client/strings/ru.json index e1a2bf5e..49d67fbe 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -32,6 +32,8 @@ "ButtonHide": "Скрыть", "ButtonHome": "Домой", "ButtonIssues": "Проблемы", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Последнее", "ButtonLibrary": "Библиотека", "ButtonLogout": "Выход", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Найти всех авторов", "ButtonMatchBooks": "Найти книги", "ButtonNevermind": "Не важно", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Открыть канал", "ButtonOpenManager": "Открыть менеджер", + "ButtonPause": "Pause", "ButtonPlay": "Слушать", "ButtonPlaying": "Проигрывается", "ButtonPlaylists": "Плейлисты", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Очистить весь кэш", "ButtonPurgeItemsCache": "Очистить кэш элементов", "ButtonPurgeMediaProgress": "Очистить прогресс медиа", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Элементы коллекции", "HeaderCover": "Обложка", "HeaderCurrentDownloads": "Текущие закачки", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Подробности", "HeaderDownloadQueue": "Очередь скачивания", "HeaderEbookFiles": "Файлы e-книг", diff --git a/client/strings/sv.json b/client/strings/sv.json index 40594dd3..3cf645b5 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -32,6 +32,8 @@ "ButtonHide": "Dölj", "ButtonHome": "Hem", "ButtonIssues": "Problem", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "Senaste", "ButtonLibrary": "Bibliotek", "ButtonLogout": "Logga ut", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "Matcha alla författare", "ButtonMatchBooks": "Matcha böcker", "ButtonNevermind": "Glöm det", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "Okej", "ButtonOpenFeed": "Öppna flöde", "ButtonOpenManager": "Öppna Manager", + "ButtonPause": "Pause", "ButtonPlay": "Spela", "ButtonPlaying": "Spelar", "ButtonPlaylists": "Spellistor", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Rensa all cache", "ButtonPurgeItemsCache": "Rensa föremåls-cache", "ButtonPurgeMediaProgress": "Rensa medieförlopp", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "Samlingselement", "HeaderCover": "Omslag", "HeaderCurrentDownloads": "Aktuella nedladdningar", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Detaljer", "HeaderDownloadQueue": "Nedladdningskö", "HeaderEbookFiles": "E-boksfiler", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 9212b998..17b3cd6a 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -32,6 +32,8 @@ "ButtonHide": "隐藏", "ButtonHome": "首页", "ButtonIssues": "问题", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", "ButtonLatest": "最新", "ButtonLibrary": "媒体库", "ButtonLogout": "注销", @@ -41,12 +43,15 @@ "ButtonMatchAllAuthors": "匹配所有作者", "ButtonMatchBooks": "匹配图书", "ButtonNevermind": "没有关系", + "ButtonNextChapter": "Next Chapter", "ButtonOk": "确定", "ButtonOpenFeed": "打开源", "ButtonOpenManager": "打开管理器", + "ButtonPause": "Pause", "ButtonPlay": "播放", "ButtonPlaying": "正在播放", "ButtonPlaylists": "播放列表", + "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "清理所有缓存", "ButtonPurgeItemsCache": "清理项目缓存", "ButtonPurgeMediaProgress": "清理媒体进度", @@ -104,6 +109,7 @@ "HeaderCollectionItems": "收藏项目", "HeaderCover": "封面", "HeaderCurrentDownloads": "当前下载", + "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "详情", "HeaderDownloadQueue": "下载队列", "HeaderEbookFiles": "电子书文件", From e0c136491668305b4af139ade300031fff4d4682 Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Thu, 15 Feb 2024 19:08:05 -0300 Subject: [PATCH 0359/2145] Create pt-br.json 540 linhas iniciais --- client/strings/pt-br.json | 767 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 767 insertions(+) create mode 100644 client/strings/pt-br.json diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json new file mode 100644 index 00000000..ded3683e --- /dev/null +++ b/client/strings/pt-br.json @@ -0,0 +1,767 @@ +{ + "ButtonAdd": "Adicionar", + "ButtonAddChapters": "Adicionar Capítulos", + "ButtonAddDevice": "Adicionar Dispositivo", + "ButtonAddLibrary": "Adicionar Biblioteca", + "ButtonAddPodcasts": "Adicionar Podcasts", + "ButtonAddUser": "Adicionar Usuário", + "ButtonAddYourFirstLibrary": "Adicionar sua primeira biblioteca", + "ButtonApply": "Aplicar", + "ButtonApplyChapters": "Aplicar Capítulos", + "ButtonAuthors": "Autores", + "ButtonBrowseForFolder": "Procurar por Pasta", + "ButtonCancel": "Cancelar", + "ButtonCancelEncode": "Cancelar Codificação", + "ButtonChangeRootPassword": "Alterar senha do administrador", + "ButtonCheckAndDownloadNewEpisodes": "Verificar & Baixar Novos Episódios", + "ButtonChooseAFolder": "Escolha uma pasta", + "ButtonChooseFiles": "Escolha arquivos", + "ButtonClearFilter": "Limpar Filtro", + "ButtonCloseFeed": "Fechar Feed", + "ButtonCollections": "Coleções", + "ButtonConfigureScanner": "Configurar Scanner", + "ButtonCreate": "Criar", + "ButtonCreateBackup": "Criar Backup", + "ButtonDelete": "Apagar", + "ButtonDownloadQueue": "Fila de download", + "ButtonEdit": "Editar", + "ButtonEditChapters": "Editar Capítulos", + "ButtonEditPodcast": "Editar Podcast", + "ButtonForceReScan": "Forcar Nova Verificação", + "ButtonFullPath": "Caminho Completo", + "ButtonHide": "Ocultar", + "ButtonHome": "Principal", + "ButtonIssues": "Problemas", + "ButtonJumpBackward": "Retroceder", + "ButtonJumpForward": "Adiantar", + "ButtonLatest": "Mais Recentes", + "ButtonLibrary": "Biblioteca", + "ButtonLogout": "Logout", + "ButtonLookup": "Procurar", + "ButtonManageTracks": "Gerenciar Faixas", + "ButtonMapChapterTitles": "Designar Títulos de Capítulos", + "ButtonitensAllAuthors": "Consultar Todos Autores", + "ButtonitensBooks": "Consultar Livros", + "ButtonNevermind": "Cancelar", + "ButtonNextChapter": "Próximo Capítulo", + "ButtonOk": "Ok", + "ButtonOpenFeed": "Abrir Feed", + "ButtonOpenManager": "Abrir Gerenciador", + "ButtonPause": "Pausar", + "ButtonPlay": "Reproduzir", + "ButtonPlaying": "Reproduzindo", + "ButtonPlaylists": "Lista de Reprodução", + "ButtonPreviousChapter": "Capítulo Anterior", + "ButtonPurgeAllCache": "Apagar Todo o Cache", + "ButtonPurgeItemsCache": "Apagar o Cache de Itens", + "ButtonPurgeMediaProgress": "Apagar o Progresso nas Mídias", + "ButtonQueueAddItem": "Adicionar à Lista", + "ButtonQueueRemoveItem": "Remover da Lista", + "ButtonQuickitens": "Consulta rápida", + "ButtonRead": "Ler", + "ButtonRemove": "Remover", + "ButtonRemoveAll": "Remover Todos", + "ButtonRemoveAllLibraryItems": "Remover Todos os Itens da Biblioteca", + "ButtonRemoveFromContinueListening": "Remover de Continuar Escutando", + "ButtonRemoveFromContinueReading": "Remover de Continuar Lendo", + "ButtonRemoveSeriesFromContinueSeries": "Remover Série de Continuar Série", + "ButtonReScan": "Nova Verificação", + "ButtonReset": "Resetar", + "ButtonResetToDefault": "Resetar para valores padrão", + "ButtonRestore": "Restaurar", + "ButtonSave": "Salvar", + "ButtonSaveAndClose": "Salvar & Fechar", + "ButtonSaveTracklist": "Salvar Lista de Faixas", + "ButtonScan": "Verificar", + "ButtonScanLibrary": "Verificar Biblioteca", + "ButtonSearch": "Pesquisar", + "ButtonSelectFolderPath": "Selecionar Caminho da Pasta", + "ButtonSeries": "Séries", + "ButtonSetChaptersFromTracks": "Definir Capítulos Segundo Faixas", + "ButtonShiftTimes": "Deslocar tempos", + "ButtonShow": "Exibir", + "ButtonStartM4BEncode": "Iniciar Codificação M4B", + "ButtonStartMetadataEmbed": "Iniciar Gravação de Metadados", + "ButtonSubmit": "Enviar", + "ButtonTest": "Testar", + "ButtonUpload": "Upload", + "ButtonUploadBackup": "Upload de Backup", + "ButtonUploadCover": "Upload de Capa", + "ButtonUploadOPMLFile": "Upload Arquivo OPML", + "ButtonUserDelete": "Apagar usuário {0}", + "ButtonUserEdit": "Editar usuário {0}", + "ButtonViewAll": "Ver tudo", + "ButtonYes": "Sim", + "ErrorUploadFetchMetadataAPI": "Erro buscando metadados", + "ErrorUploadFetchMetadataNoResults": "Não foi possível buscar metadados - tente atualizar o título e/ou autor", + "ErrorUploadLacksTitle": "É preciso ter um título", + "HeaderAccount": "Conta", + "HeaderAdvanced": "Avançado", + "HeaderAppriseNotificationSettings": "Configuração de notificações Apprise", + "HeaderAudiobookTools": "Ferramentas de Gerenciamento de Arquivos de Audiobooks", + "HeaderAudioTracks": "Trilhas de áudio", + "HeaderAuthentication": "Autenticação", + "HeaderBackups": "Backups", + "HeaderChangePassword": "Trocar Senha", + "HeaderChapters": "Capítulos", + "HeaderChooseAFolder": "Escolha uma Pasta", + "HeaderCollection": "Coleção", + "HeaderCollectionItems": "Itends da Coleção", + "HeaderCover": "Capas", + "HeaderCurrentDownloads": "Downloads em andamento", + "HeaderCustomMetadataProviders": "Fontes de Metadados Customizados", + "HeaderDetails": "Detalhes", + "HeaderDownloadQueue": "Fila de Download", + "HeaderEbookFiles": "Arquivos Ebook", + "HeaderEmail": "Email", + "HeaderEmailSettings": "Configurações de Email", + "HeaderEpisodes": "Episódios", + "HeaderEreaderDevices": "Dispositivos Ereader", + "HeaderEreaderSettings": "Configurações Ereader", + "HeaderFiles": "Arquivos", + "HeaderFindChapters": "Localizar Capítulos", + "HeaderIgnoredFiles": "Arquivos Ignorados", + "HeaderItemFiles": "Arquivos de Itens", + "HeaderItemMetadataUtils": "Utilidades para Metadados de Itens", + "HeaderLastListeningSession": "Última sessão", + "HeaderLatestEpisodes": "Últimos episódios", + "HeaderLibraries": "Bibliotecas", + "HeaderLibraryFiles": "Arquivos da Biblioteca", + "HeaderLibraryStats": "Estatisticas da Biblioteca", + "HeaderListeningSessions": "Sessões", + "HeaderListeningStats": "Estatísticas", + "HeaderLogin": "Login", + "HeaderLogs": "Logs", + "HeaderManageGenres": "Gerenciar Gêneros", + "HeaderManageetiquetas": "Gerenciar Etiquetas", + "HeaderMapDetails": "Designar Detalhes", + "Headeritens": "Consultar", + "HeaderMetadataOrderOfPrecedence": "Ordem de Prioridade dos Metadados", + "HeaderMetadataToEmbed": "Metadados a Serem Gravados", + "HeaderNewAccount": "Nova Conta", + "HeaderNewLibrary": "Nova Biblioteca", + "HeaderNotifications": "Notificações", + "HeaderOpenIDConnectAuthentication": "Autenticação via OpenID Connect", + "HeaderOpenRSSFeed": "Abrir Feed RSS", + "HeaderOtherFiles": "Outros Arquivos", + "HeaderPasswordAuthentication": "Autenticação por Senha", + "HeaderPermissions": "Permissões", + "HeaderPlayerQueue": "Fila do reprodutor", + "HeaderPlaylist": "Lista de Reprodução", + "HeaderPlaylistItems": "Intens da lista de reprodução", + "HeaderPodcastsToAdd": "Podcasts para Adicionar", + "HeaderPreviewCover": "Visualização da Capa", + "HeaderRemoveEpisode": "Remover Episódio", + "HeaderRemoveEpisodes": "Remover {0} Episódios", + "HeaderRSSFeedGeneral": "Detalhes RSS", + "HeaderRSSFeedIsOpen": "Feed RSS está aberto", + "HeaderRSSFeeds": "Feeds RSS", + "HeaderSavedMediaProgress": "Progresso da gravação das mídias", + "HeaderSchedule": "Programação", + "HeaderScheduleLibraryScans": "Programar Verificação Automática da Biblioteca", + "HeaderSession": "Sessão", + "HeaderSetBackupSchedule": "Definir Programação de Backup", + "HeaderSettings": "Configurações", + "HeaderSettingsDisplay": "Exibição", + "HeaderSettingsExperimental": "Funcionalidades experimentais", + "HeaderSettingsGeneral": "Geral", + "HeaderSettingsScanner": "Scanner", + "HeaderSleepTimer": "Timer", + "HeaderStatsLargestItems": "Maiores Itens", + "HeaderStatsLongestItems": "Itens mais longos (hrs)", + "HeaderStatsMinutesListeningChart": "Minutos Escutados (últimos 7 dias)", + "HeaderStatsRecentSessions": "Sessões Recentes", + "HeaderStatsTop10Authors": "Top 10 Autores", + "HeaderStatsTop5Genres": "Top 5 Gêneros", + "HeaderTableOfContents": "Sumário", + "HeaderTools": "Ferramentas", + "HeaderUpdateAccount": "Atualizar Conta", + "HeaderUpdateAuthor": "Atualizar Autor", + "HeaderUpdateDetails": "Atualizar Detalhes", + "HeaderUpdateLibrary": "Atualizar Biblioteca", + "HeaderUsers": "Usuários", + "HeaderYourStats": "Suas Estatísticas", + "LabelAbridged": "Versão Abreviada", + "LabelAccountType": "Tipo de Conta", + "LabelAccountTypeAdmin": "Administrador", + "LabelAccountTypeGuest": "Convidado", + "LabelAccountTypeUser": "Usuário", + "LabelActivity": "Atividade", + "LabelAdded": "Acrescentado", + "LabelAddedAt": "Acrescentado em", + "LabelAddToCollection": "Adicionar à Coleção", + "LabelAddToCollectionBatch": "Adicionar {0} Livros à Coleçào", + "LabelAddToPlaylist": "Adicionar à Lista de Reprodução", + "LabelAddToPlaylistBatch": "Adicionar {0} itens to Lista de Reprodução", + "LabelAdminUsersOnly": "Apenas usuários administradores", + "LabelAll": "Todos", + "LabelAllUsers": "Todos Usuários", + "LabelAllUsersExcludingGuests": "Todos usuários exceto convidados", + "LabelAllUsersIncludingGuests": "Todos usuários incluindo convidados", + "LabelAlreadyInYourLibrary": "Já na sua biblioteca", + "LabelAppend": "Acrescentar", + "LabelAuthor": "Autor", + "LabelAuthorFirstLast": "Autor (Nome Sobrenome)", + "LabelAuthorLastFirst": "Autor (Sobrenome, Nome)", + "LabelAuthors": "Autores", + "LabelAutoDownloadEpisodes": "Download Automático de Episódios", + "LabelAutoFetchMetadata": "Buscar Metadados Automaticamente", + "LabelAutoFetchMetadataHelp": "Busca metadados de título, autor e série para otimizar o upload. Pode ser necessário buscas metadados adicionais após o upload.", + "LabelAutoLaunch": "Iniciar Automaticamente", + "LabelAutoLaunchDescription": "Redireciona para o fornecedor de autenticação automaticamente ao navegar para a tela de login (caminhp para substituição manual <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Registrar Automaticamente", + "LabelAutoRegisterDescription": "Registra automaticamente novos usuários após login", + "LabelBackToUser": "Voltar para Usuário", + "LabelBackupLocation": "Localização do Backup", + "LabelBackupsEnableAutomaticBackups": "Ativar backups automáticos", + "LabelBackupsEnableAutomaticBackupsHelp": "Backups salvos em /metadata/backups", + "LabelBackupsMaxBackupSize": "Tamanho máximo do backup (em GB)", + "LabelBackupsMaxBackupSizeHelp": "Como proteção contra uma configuração incorreta, backups darão erro se excederem o tamanho configurado.", + "LabelBackupsNumberToKeep": "Número de backups para guardar", + "LabelBackupsNumberToKeepHelp": "Apenas 1 backup será removido por vez, então, se já existem mais backups, você deve apagá-los manualmente.", + "LabelBitrate": "Bitrate", + "LabelBooks": "Livros", + "LabelButtonText": "Texto do botão", + "LabelChangePassword": "Trocar Senha", + "LabelChannels": "Canais", + "LabelChapters": "Capítulos", + "LabelChaptersFound": "capítulos encontrados", + "LabelChapterTitle": "Título do Capítulo", + "LabelClickForMoreInfo": "Clique para mais informações", + "LabelClosePlayer": "Fechar Reprodutor", + "LabelCodec": "Codec", + "LabelCollapseSeries": "Fechar Séries", + "LabelCollection": "Coleção", + "LabelCollections": "Coleções", + "LabelComplete": "Completo", + "LabelConfirmPassword": "Confirmar Senha", + "LabelContinueListening": "Continuar Escutando", + "LabelContinueReading": "Continuar Lendo", + "LabelContinueSeries": "Continuar Série", + "LabelCover": "Capa", + "LabelCoverImageURL": "URL da Imagem da Capa", + "LabelCreatedAt": "Criado em", + "LabelCronExpression": "Expressão para o Cron", + "LabelCurrent": "Atual", + "LabelCurrently": "Atualmente:", + "LabelCustomCronExpression": "Expressão personalizada para o Cron:", + "LabelDatetime": "Data e Hora", + "LabelDeleteFromFileSystemCheckbox": "Apagar do sistema de arquivos (desmarcar para remover apenas da base de dados)", + "LabelDescription": "Descrição", + "LabelDeselectAll": "Desmarcar tudo", + "LabelDevice": "Dispositivo", + "LabelDeviceInfo": "Informação do Dispositivo", + "LabelDeviceIsAvailableTo": "Dispositivo está disponível para...", + "LabelDirectory": "Diretório", + "LabelDiscFromFilename": "Disco a partir do nome do arquivo", + "LabelDiscFromMetadata": "Disco a partir dos metadados", + "LabelDiscover": "Descobrir", + "LabelDownload": "Download", + "LabelDownloadNEpisodes": "Download de {0} Episódios", + "LabelDuration": "Duração", + "LabelDurationFound": "Duração comprovada:", + "LabelEbook": "Ebook", + "LabelEbooks": "Ebooks", + "LabelEdit": "Editar", + "LabelEmail": "Email", + "LabelEmailSettingsFromAddress": "Remetente", + "LabelEmailSettingsSecure": "Seguro", + "LabelEmailSettingsSecureHelp": "Se ativado, a conexão utilizará TLS para a conexão ao servidor. Se desativado TLS será usado se o servidor suportar a extensão STARTTLS. Na maioria dos casos ative esse valor se estiver conectando pela porta 465. Para portas 587 ou 25, mantenha inativo. (de nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Endereço de teste", + "LabelEmbeddedCover": "Capa Integrada", + "LabelEnable": "Habilitar", + "LabelEnd": "Fim", + "LabelEpisode": "Episódio", + "LabelEpisodeTitle": "Título do Episódio", + "LabelEpisodeType": "Tipo do Episódio", + "LabelExample": "Exemplo", + "LabelExplicit": "Explícito", + "LabelFeedURL": "URL do Feed", + "LabelFetchingMetadata": "Buscando Metadados", + "LabelFile": "Arquivo", + "LabelFileBirthtime": "Criação do Arquivo", + "LabelFileModified": "Modificação do Arquivo", + "LabelFilename": "Nome do Arquivo", + "LabelFilterByUser": "Filtrar por Usuário", + "LabelFindEpisodes": "Localizar Episódios", + "LabelFinished": "Terminado", + "LabelFolder": "Pasta", + "LabelFolders": "Pastas", + "LabelFontBold": "Negrito", + "LabelFontFamily": "Família de fonte", + "LabelFontItalic": "Itálico", + "LabelFontScale": "Escala de fonte", + "LabelFontStrikethrough": "Tachado", + "LabelFormat": "Formato", + "LabelGenre": "Gênero", + "LabelGenres": "Gêneros", + "LabelHardDeleteFile": "Apagar definitivamente", + "LabelHasEbook": "Tem ebook", + "LabelHasSupplementaryEbook": "Tem ebook complementar", + "LabelHighestPriority": "Prioridade mais alta", + "LabelHost": "Host", + "LabelHour": "Hora", + "LabelIcon": "Ícone", + "LabelImageURLFromTheWeb": "URL da imagem na internet", + "LabelIncludeInTracklist": "Incluir na Lista de Faixas", + "LabelIncomplete": "Incompleto", + "LabelInProgress": "Em Andamento", + "LabelInterval": "Intervalo", + "LabelIntervalCustomDailyWeekly": "Personalizar diário/semanal", + "LabelIntervalEvery12Hours": "A cada 12 horas", + "LabelIntervalEvery15Minutes": "A cada 15 minutos", + "LabelIntervalEvery2Hours": "A cada 2 horas", + "LabelIntervalEvery30Minutes": "A cada 30 minutos", + "LabelIntervalEvery6Hours": "A cada 6 horas", + "LabelIntervalEveryDay": "Todo dia", + "LabelIntervalEveryHour": "Toda hora", + "LabelInvalidParts": "Partes Inválidas", + "LabelInvert": "Inverter", + "LabelItem": "Item", + "LabelLanguage": "Idioma", + "LabelLanguageDefaultServer": "Idoma Padrão do Servidor", + "LabelLastBookAdded": "Último Livro Acrescentado", + "LabelLastBookUpdated": "Último Livro Atualizado", + "LabelLastSeen": "Visto pela Última Vez", + "LabelLastTime": "Progresso", + "LabelLastUpdate": "Última Atualização", + "LabelLayout": "Layout", + "LabelLayoutSinglePage": "Uma página", + "LabelLayoutSplitPage": "Página dividida", + "LabelLess": "Menos", + "LabelLibrariesAccessibleToUser": "Bibliotecas Acessíveis ao Usuário", + "LabelLibrary": "Biblioteca", + "LabelLibraryItem": "Item da Biblioteca", + "LabelLibraryName": "Nome da Biblioteca", + "LabelLimit": "Limite", + "LabelLineSpacing": "Espaçamento entre linhas", + "LabelListenAgain": "Ouvir novamente", + "LabelLogLevelDebug": "Debug", + "LabelLogLevelInfo": "Info", + "LabelLogLevelWarn": "Atenção", + "LabelLookForNewEpisodesAfterDate": "Procurar por novos Episódios após essa data", + "LabelLowestPriority": "Prioridade mais baixa", + "LabelMatchExistingUsersBy": "Consultar usuários existentes usando", + "LabelMatchExistingUsersByDescription": "Utilizado para conectar usuários já existentes. Uma vez conectados, usuários serão consultados utilizando uma identificação única do seu provedor de SSO", + "LabelMediaPlayer": "Reprodutor de mídia", + "LabelMediaType": "Típo de Mídia", + "LabelMetadataOrderOfPrecedenceDescription": "Fontes de metadados de alta prioridade terão preferência sobre as fontes de metadados de prioridade baixa", + "LabelMetadataProvider": "Fonte de Metadados", + "LabelMetaTag": "Etiqueta Meta", + "LabelMetaTags": "Etiquetas Meta", + "LabelMinute": "Minuto", + "LabelMissing": "Ausente", + "LabelMissingParts": "Partes Ausentes", + "LabelMobileRedirectURIs": "URIs de redirecionamento móveis permitidas", + "LabelMobileRedirectURIsDescription": "Essa é uma lista de permissionamento para URIs válidas para o redirecionamento de aplicativos móveis. A padrão é <code>audiobookshelf://oauth</code>, que pode ser removida ou acrescentada com novas URIs para integração com apps de tereceiros. Usando um asterisco (<code>*</code>) como um item único dará permissão para qualquer URI.", + "LabelMore": "Mais", + "LabelMoreInfo": "Mais Informações", + "LabelName": "Nome", + "LabelNarrator": "Narrador", + "LabelNarrators": "Narradores", + "LabelNew": "Novo", + "LabelNewestAuthors": "Novos Autores", + "LabelNewestEpisodes": "Episódios mais recentes", + "LabelNewPassword": "Nova Senha", + "LabelNextBackupDate": "Data do próximo backup", + "LabelNextScheduledRun": "Próxima execução programada", + "LabelNoEpisodesSelected": "Nenhum episódio selecionado", + "LabelNotes": "Notas", + "LabelNotFinished": "Não terminado", + "LabelNotificationAppriseURL": "URL(s) Apprise", + "LabelNotificationAvailableVariables": "Variáveis disponíveis", + "LabelNotificationBodyTemplate": "Modelo de Corpo", + "LabelNotificationEvent": "Evento de Notificação", + "LabelNotificationsMaxFailedAttempts": "Máximo de tentativas com falhas", + "LabelNotificationsMaxFailedAttemptsHelp": "Notificações serão desabilitadas após falharem este número de vezes", + "LabelNotificationsMaxQueueSize": "Tamanho máximo da fila de eventos de notificação", + "LabelNotificationsMaxQueueSizeHelp": "Eventos estão limitados a um disparo por segundo. Eventos serão ignorados se a fila estiver no tamanho máximo. Isso evita o excesso de notificações.", + "LabelNotificationTitleTemplate": "Modelo de Título", + "LabelNotStarted": "Não iniciado", + "LabelNumberOfBooks": "Número de Livros", + "LabelNumberOfEpisodes": "# de Episódios", + "LabelOpenRSSFeed": "Abrir Feed RSS", + "LabelOverwrite": "Sobrescrever", + "LabelPassword": "Senha", + "LabelPath": "Caminho", + "LabelPermissionsAccessAllLibraries": "Pode Acessar Todas Bibliotecas", + "LabelPermissionsAccessAllTags": "Pode Acessar Todas as Etiquetas", + "LabelPermissionsAccessExplicitContent": "Pode Acessar Conteúdos Explícitos", + "LabelPermissionsDelete": "Pode Apagar", + "LabelPermissionsDownload": "Pode Fazer Download", + "LabelPermissionsUpdate": "Pode Atualizar", + "LabelPermissionsUpload": "Pode Fazer Upload", + "LabelPhotoPathURL": "Caminho/URL para Foto", + "LabelPlaylists": "Listas de Reprodução", + "LabelPlayMethod": "Método de Reprodução", + "LabelPodcast": "Podcast", + "LabelPodcasts": "Podcasts", + "LabelPodcastType": "Tipo de Podcast", + "LabelPort": "Porta", + "LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)", + "LabelPreventIndexing": "Evitar que o seu feed seja indexado pelos diretórios de podcast do iTunes and Google", + "LabelPrimaryEbook": "Ebook principal", + "LabelProgress": "Progresso", + "LabelProvider": "Fonte", + "LabelPubDate": "Data de Publicação", + "LabelPublisher": "Editora", + "LabelPublishYear": "Ano de Publicação", + "LabelRead": "Lido", + "LabelReadAgain": "Ler novamente", + "LabelReadEbookWithoutProgress": "Ler ebook sem armazenar progresso", + "LabelRecentlyAdded": "Recentemente Acrescentado", + "LabelRecentSeries": "Séries Recentes", + "LabelRecommended": "Recomendado", + "LabelRedo": "Refazer", + "LabelRegion": "Região", + "LabelReleaseDate": "Data de Lançamento", + "LabelRemoveCover": "Remover capa", + "LabelRowsPerPage": "Linhas por Página", + "LabelRSSFeedCustomOwnerEmail": "Email do dono personalizado", + "LabelRSSFeedCustomOwnerName": "Nome do dono personalizado", + "LabelRSSFeedOpen": "Feed RSS Aberto", + "LabelRSSFeedPreventIndexing": "Impedir Indexação", + "LabelRSSFeedSlug": "Slug do Feed RSS", + "LabelRSSFeedURL": "URL do Feed RSS", + "LabelSearchTerm": "Busca por Termo", + "LabelSearchTitle": "Busca por Título", + "LabelSearchTitleOrASIN": "Busca por Título ou ASIN", + "LabelSeason": "Temporada", + "LabelSelectAllEpisodes": "Selecionar todos os Episódios", + "LabelSelectEpisodesShowing": "Selecionar os {0} Episódios Visíveis", + "LabelSelectUsers": "Selecionar usuários", + "LabelSendEbookToDevice": "Enviar Ebook para...", + "LabelSequence": "Sequência", + "LabelSeries": "Série", + "LabelSeriesName": "Nome da Série", + "LabelSeriesProgress": "Progresso da Série", + "LabelSetEbookAsPrimary": "Definir como principal", + "LabelSetEbookAsSupplementary": "Definir como complementar", + "LabelSettingsAudiobooksOnly": "Apenas Audiobooks", + "LabelSettingsAudiobooksOnlyHelp": "Ao ativar essa configuração os arquivos de ebooks serão ignorados a não ser que estejam dentro de uma pasta com um audiobook. Nesse caso eles serão definidos como ebooks complementares", + "LabelSettingsBookshelfViewHelp": "Aparência esqueomorfa com prateleiras de madeira", + "LabelSettingsChromecastSupport": "Suporte ao Chromecast", + "LabelSettingsDateFormat": "Formato de data", + "LabelSettingsDisableWatcher": "Desativar Monitoramento", + "LabelSettingsDisableWatcherForLibrary": "Desativa o monitoramento de pastas para a biblioteca", + "LabelSettingsDisableWatcherHelp": "Desativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor", + "LabelSettingsEnableWatcher": "Ativar Monitoramento", + "LabelSettingsEnableWatcherForLibrary": "Ativa o monitoramento de pastas para a biblioteca", + "LabelSettingsEnableWatcherHelp": "Ativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor", + "LabelSettingsExperimentalFeatures": "Funcionalidade experimentais", + "LabelSettingsExperimentalFeaturesHelp": "Funcionalidade em desenvolvimento que se beneficiairam dos seus comentários e da sua ajuda para testar. Clique para abrir a discussão no github.", + "LabelSettingsFindCovers": "Localizar capas", + "LabelSettingsFindCoversHelp": "Se o seu audiobook não tiver uma capa incluída ou uma imagem de capa na pasta, o scanner tentará localizar uma capa.<br>Atenção: Isso irá estender o tempo de análise", + "LabelSettingsHideSingleBookSeries": "Ocultar séries com um só livro", + "LabelSettingsHideSingleBookSeriesHelp": "Séries com um só livro serão ocultadas na página de séries e na prateleira de séries na página principal.", + "LabelSettingsHomePageBookshelfView": "Usar visão estante na página principal", + "LabelSettingsLibraryBookshelfView": "Usar visão estante na página da biblioteca", + "LabelSettingsParseSubtitles": "Analisar subtítulos", + "LabelSettingsParseSubtitlesHelp": "Extrair subtítulos do nome da pasta do audiobook.<br>Subtítulo deve estar separado por \" - \"<br>ex: \"Título do Livro - Um Subtítulo Aqui\" tem o subtítulo \"Um Subtítulo Aqui\"", + "LabelSettingsPreferMatchedMetadata": "Preferir metadados consultados", + "LabelSettingsPreferMatchedMetadataHelp": "Dados consultados serão priorizados sobre os detalhes do item quando usada a Consulta Rápida. Por padrão, Consulta Rápida só preencherá os detalhes ausentes.", + "LabelSettingsSkipMatchingBooksWithASIN": "Pular consulta de livros que já têm um ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Pular consulta de livros que já têm um ISBN", + "LabelSettingsSortingIgnorePrefixes": "Ignorar prefixos ao ordenar", + "LabelSettingsSortingIgnorePrefixesHelp": "ex: o prefixo \"o\" do título \"O Título do Livro\" seria ordenado como \"Título do Livro, O\"", + "LabelSettingsSquareBookCovers": "Usar capas de livro quadradas", + "LabelSettingsSquareBookCoversHelp": "Preferir capas quadradas ao invés das capas 1.6:1 padrão", + "LabelSettingsStoreCoversWithItem": "Armazenar capas com o item", + "LabelSettingsStoreCoversWithItemHelp": "Por padrão as capas são armazenadas em /metadata/items. Ao ativar essa configuração as capas serão armazenadas na pasta do item na sua biblioteca. Apenas um arquivo chamado \"cover\" será mantido", + "LabelSettingsStoreMetadataWithItem": "Armazenar metadados com o item", + "LabelSettingsStoreMetadataWithItemHelp": "Por padrão os arquivos de metadados são armazenados em /metadata/items. Ao ativar essa configuração os arquivos de metadados serão armazenadas nas pastas dos itens na sua biblioteca", + "LabelSettingsTimeFormat": "Formato do Tempo", + "LabelShowAll": "Mostrar Todos", + "LabelSize": "Tamanho", + "LabelSleepTimer": "Timer", + "LabelSlug": "Slug", + "LabelStart": "Iniciar", + "LabelStarted": "Iniciado", + "LabelStartedAt": "Iniciado Em", + "LabelStartTime": "Horário do Início", + "LabelStatsAudioTracks": "Trilhas de Áudio", + "LabelStatsAuthors": "Autores", + "LabelStatsBestDay": "Melhor Dia", + "LabelStatsDailyAverage": "Média Diária", + "LabelStatsDays": "Dias", + "LabelStatsDaysListened": "Dias Escutando", + "LabelStatsHours": "Horas", + "LabelStatsInARow": "seguidas", + "LabelStatsItemsFinished": "itens Concluídos", + "LabelStatsItemsInLibrary": "itens na biblioteca", + "LabelStatsMinutes": "minutos", + "LabelStatsMinutesListening": "Minutos Escutando", + "LabelStatsOverallDays": "Total de Dias", + "LabelStatsOverallHours": "Total de Horas", + "LabelStatsWeekListening": "Tempo escutando na semana", + "LabelSubtitle": "Subtítulo", + "LabelSupportedFileTypes": "Tipos de arquivos suportados", + "LabelTag": "Etiqueta", + "LabelTags": "Etiquetas", + "LabelTagsAccessibleToUser": "Etiquetas Acessíveis ao Usuário", + "LabelTagsNotAccessibleToUser": "Etiquetas não Acessíveis Usuário", + "LabelTasks": "Tarefas em Execuçào", + "LabelTextEditorBulletedList": "Lista com marcadores", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Lista numerada", + "LabelTextEditorUnlink": "Remover link", + "LabelTheme": "Tema", + "LabelThemeDark": "Escuro", + "LabelThemeLight": "Claro", + "LabelTimeBase": "Base de tempo", + "LabelTimeListened": "Tempo de escuta", + "LabelTimeListenedToday": "Tempo de escuta hoje", + "LabelTimeRemaining": "{0} restantes", + "LabelTimeToShift": "Deslocamento de tempo em segundos", + "LabelTitle": "Título", + "LabelToolsEmbedMetadata": "Incluir Metadados", + "LabelToolsEmbedMetadataDescription": "Incuir metadados no arquivo de áudio, com imagem da capa e capítulos.", + "LabelToolsMakeM4b": "Gerar audiobook no formato M4B", + "LabelToolsMakeM4bDescription": "Gerar um arquivo de audiobook no formato .M4B com metadados, imagem da capa e capítulos.", + "LabelToolsSplitM4b": "Dividir um M4B em MP3s", + "LabelToolsSplitM4bDescription": "Criar arquivos MP3s a partir da divisão de um M4B em capítulos, com metadados e imagem de capa.", + "LabelTotalDuration": "Duração Total", + "LabelTotalTimeListened": "Tempo Total Escutado", + "LabelTrackFromFilename": "Trilha a partir do nome do arquvico", + "LabelTrackFromMetadata": "Trilha a partir dos Metadados", + "LabelTracks": "Trilhas", + "LabelTracksMultiTrack": "Várias trilhas", + "LabelTracksNone": "Sem trilha", + "LabelTracksSingleTrack": "Trilha única", + "LabelType": "Tipo", + "LabelUnabridged": "Não Abreviada", + "LabelUndo": "Undo", + "LabelUnknown": "Desconhecido", + "LabelUpdateCover": "Atualizar Capa", + "LabelUpdateCoverHelp": "Permite sobrescrever capas existentes para os livros selecionados quando uma consulta for localizada", + "LabelUpdatedAt": "Atualizado em", + "LabelUpdateDetails": "Atualizar Detalhes", + "LabelUpdateDetailsHelp": "Permite sobrescrever detalhes existentes para os livros selecionados quando uma consulta for localizada", + "LabelUploaderDragAndDrop": "Arraste e solte arquivos ou pastas", + "LabelUploaderDropFiles": "Drop files", + "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", + "LabelUseChapterTrack": "Use chapter track", + "LabelUseFullTrack": "Use full track", + "LabelUser": "User", + "LabelUsername": "Username", + "LabelValue": "Value", + "LabelVersion": "Version", + "LabelViewBookmarks": "View bookmarks", + "LabelViewChapters": "View Capítulos", + "LabelViewQueue": "View player queue", + "LabelVolume": "Volume", + "LabelWeekdaysToRun": "Weekdays to run", + "LabelYourAudiobookDuration": "Your audiobook duration", + "LabelYourBookmarks": "Your Bookmarks", + "LabelYourPlaylists": "Your Lista de Reproduçãos", + "LabelYourProgress": "Your Progress", + "MessageAddToPlayerQueue": "Adicionar to player queue", + "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "Backups include users, user progress, Biblioteca item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your Biblioteca folders.", + "MessageBatchQuickMatchDescription": "Quick Match will attempt to Adicionar missing covers and metadata for the selected itens. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.", + "MessageBookshelfNoCollections": "You haven't made any collections yet", + "MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "No RSS feeds are open", + "MessageBookshelfNoSeries": "You have no series", + "MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook", + "MessageChapterErrorFirstNotZero": "First chapter must start at 0", + "MessageChapterErrorStartGteDuration": "Invalid start time must be less than audiobook duration", + "MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time", + "MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook", + "MessageCheckingCron": "Checking cron...", + "MessageConfirmCloseFeed": "Are you sure you want to close this feed?", + "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 Biblioteca \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "This will delete the Biblioteca item from the database and your file system. Are you sure?", + "MessageConfirmDeleteLibraryItems": "This will delete {0} Biblioteca itens 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 Episódios as finished?", + "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all Episódios 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}\"?", + "MessageConfirmRemoveEpisode": "Are you sure you want to remove Episódio \"{0}\"?", + "MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} Episódios?", + "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", + "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", + "MessageConfirmRemovePlaylist": "Are you sure you want to remove your Lista de Reprodução \"{0}\"?", + "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all itens?", + "MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.", + "MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".", + "MessageConfirmRenameTag": "Are you sure you want to rename etiqueta \"{0}\" to \"{1}\" for all itens?", + "MessageConfirmRenameTagMergeNote": "Note: This etiqueta already exists so they will be merged.", + "MessageConfirmRenameTagWarning": "Warning! A similar etiqueta with a different casing already exists \"{0}\".", + "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} itens?", + "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", + "MessageDownloadingEpisode": "Downloading Episódio", + "MessageDragFilesIntoTrackOrder": "Drag files into correct track order", + "MessageEmbedFinished": "Embed Finished!", + "MessageEpisodesQueuedForDownload": "{0} Episódio(s) queued for download", + "MessageFeedURLWillBe": "Feed URL will be {0}", + "MessageFetching": "Fetching...", + "MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 etiquetas, OPF files, and text files will be scanned as new.", + "MessageImportantNotice": "Important Notice!", + "MessageInsertChapterBelow": "Insert chapter below", + "MessageItemsSelected": "{0} itens Selected", + "MessageItemsUpdated": "{0} itens Updated", + "MessageJoinUsOn": "Join us on", + "MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year", + "MessageLoading": "Loading...", + "MessageLoadingFolders": "Loading folders...", + "MessageM4BFailed": "M4B Failed!", + "MessageM4BFinished": "M4B Finished!", + "MessageMapChapterTitles": "Map chapter titles to your existing audiobook Capítulos without adjusting timestamps", + "MessageMarkAllEpisodesFinished": "Mark all Episódios finished", + "MessageMarkAllEpisodesNotFinished": "Mark all Episódios not finished", + "MessageMarkAsFinished": "Mark as Finished", + "MessageMarkAsNotFinished": "Mark as Not Finished", + "MessageMatchBooksDescription": "will attempt to match books in the Biblioteca with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.", + "MessageNoAudioTracks": "No audio tracks", + "MessageNoAuthors": "No Authors", + "MessageNoBackups": "No Backups", + "MessageNoBookmarks": "No Bookmarks", + "MessageNoChapters": "No Capítulos", + "MessageNoCollections": "No Collections", + "MessageNoCoversFound": "No Covers Found", + "MessageNoDescription": "No description", + "MessageNoDownloadsInProgress": "No downloads currently in progress", + "MessageNoDownloadsQueued": "No downloads queued", + "MessageNoEpisodeMatchesFound": "No Episódio matches found", + "MessageNoEpisodes": "No Episódios", + "MessageNoFoldersAvailable": "No Folders Available", + "MessageNoGenres": "No Genres", + "MessageNoIssues": "No Issues", + "MessageNoItems": "No itens", + "MessageNoItemsFound": "No itens found", + "MessageNoListeningSessions": "No Listening Sessions", + "MessageNoLogs": "No Logs", + "MessageNoMediaProgress": "No Media Progress", + "MessageNoNotifications": "No Notifications", + "MessageNoPodcastsFound": "No podcasts found", + "MessageNoResults": "No Results", + "MessageNoSearchResultsFor": "No search results for \"{0}\"", + "MessageNoSeries": "No Series", + "MessageNoTags": "No etiquetas", + "MessageNoTasksRunning": "No Tasks Running", + "MessageNotYetImplemented": "Not yet implemented", + "MessageNoUpdateNecessary": "No update necessary", + "MessageNoUpdatesWereNecessary": "No updates were necessary", + "MessageNoUserPlaylists": "You have no Lista de Reproduçãos", + "MessageOr": "or", + "MessagePauseChapter": "Pause chapter playback", + "MessagePlayChapter": "Listen to beginning of chapter", + "MessagePlaylistCreateFromCollection": "Create Lista de Reprodução from collection", + "MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching", + "MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer itensed metadata' server setting is enabled.", + "MessageRemoveChapter": "Remove chapter", + "MessageRemoveEpisodes": "Remove {0} Episódio(s)", + "MessageRemoveFromPlayerQueue": "Remove from player queue", + "MessageRemoveUserWarning": "Are you sure you want to permanently delete user \"{0}\"?", + "MessageReportBugsAndContribute": "Report bugs, request features, and contribute on", + "MessageResetChaptersConfirm": "Are you sure you want to reset Capítulos and undo the changes you made?", + "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", + "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your Biblioteca folders. If you have enabled server settings to store cover art and metadata in your Biblioteca folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.", + "MessageSearchResultsFor": "Search results for", + "MessageSelected": "{0} selected", + "MessageServerCouldNotBeReached": "Server could not be reached", + "MessageSetChaptersFromTracksDescription": "Set Capítulos using each audio file as a chapter and chapter title as the audio file name", + "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?", + "MessageThinking": "Thinking...", + "MessageUploaderItemFailed": "Failed to upload", + "MessageUploaderItemSuccess": "Successfully Uploaded!", + "MessageUploading": "Uploading...", + "MessageValidCronExpression": "Valid cron expression", + "MessageWatcherIsDisabledGlobally": "Watcher is disabled globally in server settings", + "MessageXLibraryIsEmpty": "{0} Biblioteca is empty!", + "MessageYourAudiobookDurationIsLonger": "Your audiobook duration is longer than the duration found", + "MessageYourAudiobookDurationIsShorter": "Your audiobook duration is shorter than duration found", + "NoteChangeRootPassword": "Root user is the only user that can have an empty password", + "NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.", + "NoteFolderPicker": "Note: folders already mapped will not be shown", + "NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your Episódios do not have a Pub Date. Some podcast apps require this.", + "NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate Biblioteca itens.", + "NoteUploaderOnlyAudioFiles": "If uploading only audio files then each audio file will be handled as a separate audiobook.", + "NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.", + "PlaceholderNewCollection": "New collection name", + "PlaceholderNewFolderPath": "New folder path", + "PlaceholderNewPlaylist": "New Lista de Reprodução name", + "PlaceholderSearch": "Search..", + "PlaceholderSearchEpisode": "Search Episódio..", + "ToastAccountUpdateFailed": "Failed to update account", + "ToastAccountUpdateSuccess": "Account updated", + "ToastAuthorImageRemoveFailed": "Failed to remove image", + "ToastAuthorImageRemoveSuccess": "Author image removed", + "ToastAuthorUpdateFailed": "Failed to update author", + "ToastAuthorUpdateMerged": "Author merged", + "ToastAuthorUpdateSuccess": "Author updated", + "ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)", + "ToastBackupCreateFailed": "Failed to create backup", + "ToastBackupCreateSuccess": "Backup created", + "ToastBackupDeleteFailed": "Failed to delete backup", + "ToastBackupDeleteSuccess": "Backup deleted", + "ToastBackupRestoreFailed": "Failed to restore backup", + "ToastBackupUploadFailed": "Failed to upload backup", + "ToastBackupUploadSuccess": "Backup uploaded", + "ToastBatchUpdateFailed": "Batch update failed", + "ToastBatchUpdateSuccess": "Batch update success", + "ToastBookmarkCreateFailed": "Failed to create bookmark", + "ToastBookmarkCreateSuccess": "Bookmark added", + "ToastBookmarkRemoveFailed": "Failed to remove bookmark", + "ToastBookmarkRemoveSuccess": "Bookmark removed", + "ToastBookmarkUpdateFailed": "Failed to update bookmark", + "ToastBookmarkUpdateSuccess": "Bookmark updated", + "ToastChaptersHaveErrors": "Capítulos have errors", + "ToastChaptersMustHaveTitles": "Capítulos must have titles", + "ToastCollectionItemsRemoveFailed": "Failed to remove item(s) from collection", + "ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection", + "ToastCollectionRemoveFailed": "Failed to remove collection", + "ToastCollectionRemoveSuccess": "Collection removed", + "ToastCollectionUpdateFailed": "Failed to update collection", + "ToastCollectionUpdateSuccess": "Collection updated", + "ToastItemCoverUpdateFailed": "Failed to update item cover", + "ToastItemCoverUpdateSuccess": "Item cover updated", + "ToastItemDetailsUpdateFailed": "Failed to update item details", + "ToastItemDetailsUpdateSuccess": "Item details updated", + "ToastItemDetailsUpdateUnneeded": "No updates needed for item details", + "ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished", + "ToastItemMarkedAsFinishedSuccess": "Item marked as Finished", + "ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished", + "ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished", + "ToastLibraryCreateFailed": "Failed to create biblioteca", + "ToastLibraryCreateSuccess": "Biblioteca \"{0}\" created", + "ToastLibraryDeleteFailed": "Failed to delete biblioteca", + "ToastLibraryDeleteSuccess": "Biblioteca deleted", + "ToastLibraryScanFailedToStart": "Failed to start scan", + "ToastLibraryScanStarted": "Biblioteca scan started", + "ToastLibraryUpdateFailed": "Failed to update biblioteca", + "ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" updated", + "ToastPlaylistCreateFailed": "Failed to create Lista de Reprodução", + "ToastPlaylistCreateSuccess": "Lista de Reprodução created", + "ToastPlaylistRemoveFailed": "Failed to remove Lista de Reprodução", + "ToastPlaylistRemoveSuccess": "Lista de Reprodução removed", + "ToastPlaylistUpdateFailed": "Failed to update Lista de Reprodução", + "ToastPlaylistUpdateSuccess": "Lista de Reprodução updated", + "ToastPodcastCreateFailed": "Failed to create podcast", + "ToastPodcastCreateSuccess": "Podcast created successfully", + "ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection", + "ToastRemoveItemFromCollectionSuccess": "Item removed from collection", + "ToastRSSFeedCloseFailed": "Failed to close RSS feed", + "ToastRSSFeedCloseSuccess": "RSS feed closed", + "ToastSendEbookToDeviceFailed": "Failed to send ebook to dispositivo", + "ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"", + "ToastSeriesUpdateFailed": "Series update failed", + "ToastSeriesUpdateSuccess": "Series update success", + "ToastSessionDeleteFailed": "Failed to delete session", + "ToastSessionDeleteSuccess": "Session deleted", + "ToastSocketConnected": "Socket connected", + "ToastSocketDisconnected": "Socket disconnected", + "ToastSocketFailedToConnect": "Socket failed to connect", + "ToastUserDeleteFailed": "Failed to delete usuário", + "ToastUserDeleteSuccess": "User deleted" +} From af7cb2432b3480c08005abb7f557b33ebd8a185f Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 15 Feb 2024 16:46:19 -0600 Subject: [PATCH 0360/2145] Update:Log uncaught exceptions to crash_logs.txt #706 & cleanup logger --- client/pages/config/log.vue | 23 +++--- server/Logger.js | 36 +++++++--- server/Server.js | 56 ++++++++++----- server/SocketAuthority.js | 1 - server/controllers/MiscController.js | 20 +++++- server/managers/LogManager.js | 102 +++++++++++++++++++-------- server/objects/DailyLog.js | 82 +++++++++++---------- server/routers/ApiRouter.js | 1 + server/scanner/LibraryScan.js | 9 ++- 9 files changed, 223 insertions(+), 107 deletions(-) diff --git a/client/pages/config/log.vue b/client/pages/config/log.vue index a5939852..756261b7 100644 --- a/client/pages/config/log.vue +++ b/client/pages/config/log.vue @@ -8,7 +8,7 @@ </div> <div class="relative"> - <div ref="container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="max-height: 800px; min-height: 550px"> + <div ref="container" id="log-container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="min-height: 550px"> <template v-for="(log, index) in logs"> <div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`"> <p class="text-gray-400 w-36 font-mono text-xs">{{ log.timestamp }}</p> @@ -136,7 +136,15 @@ export default { this.loadedLogs = this.loadedLogs.slice(-5000) } }, - init(attempts = 0) { + async loadLoggerData() { + const loggerData = await this.$axios.$get('/api/logger-data').catch((error) => { + console.error('Failed to load logger data', error) + this.$toast.error('Failed to load logger data') + }) + + this.loadedLogs = loggerData?.currentDailyLogs || [] + }, + async init(attempts = 0) { if (!this.$root.socket) { if (attempts > 10) { return console.error('Failed to setup socket listeners') @@ -147,14 +155,11 @@ export default { return } + await this.loadLoggerData() + this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {} - this.$root.socket.on('daily_logs', this.dailyLogsLoaded) this.$root.socket.on('log', this.logEvtReceived) this.$root.socket.emit('set_log_listener', this.newServerSettings.logLevel) - this.$root.socket.emit('fetch_daily_logs') - }, - dailyLogsLoaded(lines) { - this.loadedLogs = lines } }, updated() { @@ -166,13 +171,15 @@ export default { beforeDestroy() { if (!this.$root.socket) return this.$root.socket.emit('remove_log_listener') - this.$root.socket.off('daily_logs', this.dailyLogsLoaded) this.$root.socket.off('log', this.logEvtReceived) } } </script> <style scoped> +#log-container { + height: calc(100vh - 285px); +} .logmessage { width: calc(100% - 208px); } diff --git a/server/Logger.js b/server/Logger.js index 54fa5802..ba20801f 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -3,13 +3,17 @@ const { LogLevel } = require('./utils/constants') class Logger { constructor() { + /** @type {import('./managers/LogManager')} */ + this.logManager = null + this.isDev = process.env.NODE_ENV !== 'production' this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE this.socketListeners = [] - - this.logManager = null } + /** + * @returns {string} + */ get timestamp() { return date.format(new Date(), 'YYYY-MM-DD HH:mm:ss.SSS') } @@ -23,6 +27,9 @@ class Logger { return 'UNKNOWN' } + /** + * @returns {string} + */ get source() { try { throw new Error() @@ -62,7 +69,12 @@ class Logger { this.socketListeners = this.socketListeners.filter(s => s.id !== socketId) } - handleLog(level, args) { + /** + * + * @param {number} level + * @param {string[]} args + */ + async handleLog(level, args) { const logObj = { timestamp: this.timestamp, source: this.source, @@ -71,15 +83,17 @@ class Logger { level } - if (level >= this.logLevel && this.logManager) { - this.logManager.logToFile(logObj) - } - + // Emit log to sockets that are listening to log events this.socketListeners.forEach((socketListener) => { if (socketListener.level <= level) { socketListener.socket.emit('log', logObj) } }) + + // Save log to file + if (level >= this.logLevel) { + await this.logManager.logToFile(logObj) + } } setLogLevel(level) { @@ -117,9 +131,15 @@ class Logger { this.handleLog(LogLevel.ERROR, args) } + /** + * Fatal errors are ones that exit the process + * Fatal logs are saved to crash_logs.txt + * + * @param {...any} args + */ fatal(...args) { console.error(`[${this.timestamp}] FATAL:`, ...args, `(${this.source})`) - this.handleLog(LogLevel.FATAL, args) + return this.handleLog(LogLevel.FATAL, args) } note(...args) { diff --git a/server/Server.js b/server/Server.js index 9d2e3996..6feabee8 100644 --- a/server/Server.js +++ b/server/Server.js @@ -2,6 +2,7 @@ const Path = require('path') const Sequelize = require('sequelize') const express = require('express') const http = require('http') +const util = require('util') const fs = require('./libs/fsExtra') const fileUpload = require('./libs/expressFileupload') const rateLimit = require('./libs/expressRateLimit') @@ -21,11 +22,11 @@ const SocketAuthority = require('./SocketAuthority') const ApiRouter = require('./routers/ApiRouter') const HlsRouter = require('./routers/HlsRouter') +const LogManager = require('./managers/LogManager') const NotificationManager = require('./managers/NotificationManager') const EmailManager = require('./managers/EmailManager') const AbMergeManager = require('./managers/AbMergeManager') const CacheManager = require('./managers/CacheManager') -const LogManager = require('./managers/LogManager') const BackupManager = require('./managers/BackupManager') const PlaybackSessionManager = require('./managers/PlaybackSessionManager') const PodcastManager = require('./managers/PodcastManager') @@ -67,7 +68,6 @@ class Server { this.notificationManager = new NotificationManager() this.emailManager = new EmailManager() this.backupManager = new BackupManager() - this.logManager = new LogManager() this.abMergeManager = new AbMergeManager() this.playbackSessionManager = new PlaybackSessionManager() this.podcastManager = new PodcastManager(this.watcher, this.notificationManager) @@ -81,7 +81,7 @@ class Server { this.apiRouter = new ApiRouter(this) this.hlsRouter = new HlsRouter(this.auth, this.playbackSessionManager) - Logger.logManager = this.logManager + Logger.logManager = new LogManager() this.server = null this.io = null @@ -102,10 +102,13 @@ class Server { */ async init() { Logger.info('[Server] Init v' + version) + await this.playbackSessionManager.removeOrphanStreams() await Database.init(false) + await Logger.logManager.init() + // Create token secret if does not exist (Added v2.1.0) if (!Database.serverSettings.tokenSecret) { await this.auth.initTokenSecret() @@ -115,7 +118,6 @@ class Server { await CacheManager.ensureCachePaths() await this.backupManager.init() - await this.logManager.init() await this.rssFeedManager.init() const libraries = await Database.libraryModel.getAllOldLibraries() @@ -135,8 +137,41 @@ class Server { } } + /** + * Listen for SIGINT and uncaught exceptions + */ + initProcessEventListeners() { + let sigintAlreadyReceived = false + process.on('SIGINT', async () => { + if (!sigintAlreadyReceived) { + sigintAlreadyReceived = true + Logger.info('SIGINT (Ctrl+C) received. Shutting down...') + await this.stop() + Logger.info('Server stopped. Exiting.') + } else { + Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.') + } + process.exit(0) + }) + + /** + * @see https://nodejs.org/api/process.html#event-uncaughtexceptionmonitor + */ + process.on('uncaughtExceptionMonitor', async (error, origin) => { + await Logger.fatal(`[Server] Uncaught exception origin: ${origin}, error:`, util.format('%O', error)) + }) + /** + * @see https://nodejs.org/api/process.html#event-unhandledrejection + */ + process.on('unhandledRejection', async (reason, promise) => { + await Logger.fatal(`[Server] Unhandled rejection: ${reason}, promise:`, util.format('%O', promise)) + process.exit(1) + }) + } + async start() { Logger.info('=== Starting Server ===') + this.initProcessEventListeners() await this.init() const app = express() @@ -284,19 +319,6 @@ class Server { }) app.get('/healthcheck', (req, res) => res.sendStatus(200)) - let sigintAlreadyReceived = false - process.on('SIGINT', async () => { - if (!sigintAlreadyReceived) { - sigintAlreadyReceived = true - Logger.info('SIGINT (Ctrl+C) received. Shutting down...') - await this.stop() - Logger.info('Server stopped. Exiting.') - } else { - Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.') - } - process.exit(0) - }) - this.server.listen(this.Port, this.Host, () => { if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`) else Logger.info(`Listening on port :${this.Port}`) diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 00f0a63e..b626c0e4 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -116,7 +116,6 @@ class SocketAuthority { // Logs socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level)) socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id)) - socket.on('fetch_daily_logs', () => this.Server.logManager.socketRequestDailyLogs(socket)) // Sent automatically from socket.io clients socket.on('disconnect', (reason) => { diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index c2272ee6..2badbb6e 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -699,7 +699,7 @@ class MiscController { } /** - * GET: /api/me/stats/year/:year + * GET: /api/stats/year/:year * * @param {import('express').Request} req * @param {import('express').Response} res @@ -717,5 +717,23 @@ class MiscController { const stats = await adminStats.getStatsForYear(year) res.json(stats) } + + /** + * GET: /api/logger-data + * admin or up + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async getLoggerData(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get logger data`) + return res.sendStatus(403) + } + + res.json({ + currentDailyLogs: Logger.logManager.getMostRecentCurrentDailyLogs() + }) + } } module.exports = new MiscController() diff --git a/server/managers/LogManager.js b/server/managers/LogManager.js index 0b01f32f..731dfe70 100644 --- a/server/managers/LogManager.js +++ b/server/managers/LogManager.js @@ -1,19 +1,34 @@ const Path = require('path') const fs = require('../libs/fsExtra') +const Logger = require('../Logger') const DailyLog = require('../objects/DailyLog') -const Logger = require('../Logger') +const { LogLevel } = require('../utils/constants') const TAG = '[LogManager]' +/** + * @typedef LogObject + * @property {string} timestamp + * @property {string} source + * @property {string} message + * @property {string} levelName + * @property {number} level + */ + class LogManager { constructor() { this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily') this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans') + /** @type {DailyLog} */ this.currentDailyLog = null + + /** @type {LogObject[]} */ this.dailyLogBuffer = [] + + /** @type {string[]} */ this.dailyLogFiles = [] } @@ -26,12 +41,12 @@ class LogManager { await fs.ensureDir(this.ScanLogPath) } - async ensureScanLogDir() { - if (!(await fs.pathExists(this.ScanLogPath))) { - await fs.mkdir(this.ScanLogPath) - } - } - + /** + * 1. Ensure log directories exist + * 2. Load daily log files + * 3. Remove old daily log files + * 4. Create/set current daily log file + */ async init() { await this.ensureLogDirs() @@ -46,11 +61,11 @@ class LogManager { } } + // set current daily log file or create if does not exist const currentDailyLogFilename = DailyLog.getCurrentDailyLogFilename() Logger.info(TAG, `Init current daily log filename: ${currentDailyLogFilename}`) - this.currentDailyLog = new DailyLog() - this.currentDailyLog.setData({ dailyLogDirPath: this.DailyLogPath }) + this.currentDailyLog = new DailyLog(this.DailyLogPath) if (this.dailyLogFiles.includes(currentDailyLogFilename)) { Logger.debug(TAG, `Daily log file already exists - set in Logger`) @@ -59,7 +74,7 @@ class LogManager { this.dailyLogFiles.push(this.currentDailyLog.filename) } - // Log buffered Logs + // Log buffered daily logs if (this.dailyLogBuffer.length) { this.dailyLogBuffer.forEach((logObj) => { this.currentDailyLog.appendLog(logObj) @@ -68,9 +83,12 @@ class LogManager { } } + /** + * Load all daily log filenames in /metadata/logs/daily + */ async scanLogFiles() { const dailyFiles = await fs.readdir(this.DailyLogPath) - if (dailyFiles && dailyFiles.length) { + if (dailyFiles?.length) { dailyFiles.forEach((logFile) => { if (Path.extname(logFile) === '.txt') { Logger.debug('Daily Log file found', logFile) @@ -83,30 +101,38 @@ class LogManager { this.dailyLogFiles.sort() } - async removeOldestLog() { - if (!this.dailyLogFiles.length) return - const oldestLog = this.dailyLogFiles[0] - return this.removeLogFile(oldestLog) - } - + /** + * + * @param {string} filename + */ async removeLogFile(filename) { const fullPath = Path.join(this.DailyLogPath, filename) const exists = await fs.pathExists(fullPath) if (!exists) { Logger.error(TAG, 'Invalid log dne ' + fullPath) - this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf.filename !== filename) + this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf !== filename) } else { try { await fs.unlink(fullPath) Logger.info(TAG, 'Removed daily log: ' + filename) - this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf.filename !== filename) + this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf !== filename) } catch (error) { Logger.error(TAG, 'Failed to unlink log file ' + fullPath) } } } - logToFile(logObj) { + /** + * + * @param {LogObject} logObj + */ + async logToFile(logObj) { + // Fatal crashes get logged to a separate file + if (logObj.level === LogLevel.FATAL) { + await this.logCrashToFile(logObj) + } + + // Buffer when logging before daily logs have been initialized if (!this.currentDailyLog) { this.dailyLogBuffer.push(logObj) return @@ -114,25 +140,39 @@ class LogManager { // Check log rolls to next day if (this.currentDailyLog.id !== DailyLog.getCurrentDateString()) { - const newDailyLog = new DailyLog() - newDailyLog.setData({ dailyLogDirPath: this.DailyLogPath }) - this.currentDailyLog = newDailyLog + this.currentDailyLog = new DailyLog(this.DailyLogPath) if (this.dailyLogFiles.length > this.loggerDailyLogsToKeep) { - this.removeOldestLog() + // Remove oldest log + this.removeLogFile(this.dailyLogFiles[0]) } } // Append log line to log file - this.currentDailyLog.appendLog(logObj) + return this.currentDailyLog.appendLog(logObj) } - socketRequestDailyLogs(socket) { - if (!this.currentDailyLog) { - return - } + /** + * + * @param {LogObject} logObj + */ + async logCrashToFile(logObj) { + const line = JSON.stringify(logObj) + '\n' - const lastLogs = this.currentDailyLog.logs.slice(-5000) - socket.emit('daily_logs', lastLogs) + const logsDir = Path.join(global.MetadataPath, 'logs') + await fs.ensureDir(logsDir) + const crashLogPath = Path.join(logsDir, 'crash_logs.txt') + return fs.writeFile(crashLogPath, line, { flag: "a+" }).catch((error) => { + console.log('[LogManager] Appended crash log', error) + }) + } + + /** + * Most recent 5000 daily logs + * + * @returns {string} + */ + getMostRecentCurrentDailyLogs() { + return this.currentDailyLog?.logs.slice(-5000) || '' } } module.exports = LogManager \ No newline at end of file diff --git a/server/objects/DailyLog.js b/server/objects/DailyLog.js index a4ea90bb..3eeab15a 100644 --- a/server/objects/DailyLog.js +++ b/server/objects/DailyLog.js @@ -1,23 +1,28 @@ const Path = require('path') const date = require('../libs/dateAndTime') const fs = require('../libs/fsExtra') -const { readTextFile } = require('../utils/fileUtils') +const fileUtils = require('../utils/fileUtils') const Logger = require('../Logger') class DailyLog { - constructor() { - this.id = null - this.datePretty = null + /** + * + * @param {string} dailyLogDirPath Path to daily logs /metadata/logs/daily + */ + constructor(dailyLogDirPath) { + this.id = date.format(new Date(), 'YYYY-MM-DD') - this.dailyLogDirPath = null - this.filename = null - this.path = null - this.fullPath = null + this.dailyLogDirPath = dailyLogDirPath + this.filename = this.id + '.txt' + this.fullPath = Path.join(this.dailyLogDirPath, this.filename) - this.createdAt = null + this.createdAt = Date.now() + /** @type {import('../managers/LogManager').LogObject[]} */ this.logs = [] + /** @type {string[]} */ this.bufferedLogLines = [] + this.locked = false } @@ -32,8 +37,6 @@ class DailyLog { toJSON() { return { id: this.id, - datePretty: this.datePretty, - path: this.path, dailyLogDirPath: this.dailyLogDirPath, fullPath: this.fullPath, filename: this.filename, @@ -41,36 +44,34 @@ class DailyLog { } } - setData(data) { - this.id = date.format(new Date(), 'YYYY-MM-DD') - this.datePretty = date.format(new Date(), 'ddd, MMM D YYYY') - - this.dailyLogDirPath = data.dailyLogDirPath - - this.filename = this.id + '.txt' - this.path = Path.join('backups', this.filename) - this.fullPath = Path.join(this.dailyLogDirPath, this.filename) - - this.createdAt = Date.now() - } - - async appendBufferedLogs() { - var buffered = [...this.bufferedLogLines] + /** + * Append all buffered lines to daily log file + */ + appendBufferedLogs() { + let buffered = [...this.bufferedLogLines] this.bufferedLogLines = [] - var oneBigLog = '' + let oneBigLog = '' buffered.forEach((logLine) => { oneBigLog += logLine }) - this.appendLogLine(oneBigLog) + return this.appendLogLine(oneBigLog) } - async appendLog(logObj) { + /** + * + * @param {import('../managers/LogManager').LogObject} logObj + */ + appendLog(logObj) { this.logs.push(logObj) - var line = JSON.stringify(logObj) + '\n' - this.appendLogLine(line) + return this.appendLogLine(JSON.stringify(logObj) + '\n') } + /** + * Append log to daily log file + * + * @param {string} line + */ async appendLogLine(line) { if (this.locked) { this.bufferedLogLines.push(line) @@ -84,24 +85,29 @@ class DailyLog { this.locked = false if (this.bufferedLogLines.length) { - this.appendBufferedLogs() + await this.appendBufferedLogs() } } + /** + * Load all logs from file + * Parses lines and re-saves the file if bad lines are removed + */ async loadLogs() { - var exists = await fs.pathExists(this.fullPath) - if (!exists) { + if (!await fs.pathExists(this.fullPath)) { console.error('Daily log does not exist') return } - var text = await readTextFile(this.fullPath) + const text = await fileUtils.readTextFile(this.fullPath) - var hasFailures = false + let hasFailures = false - var logLines = text.split(/\r?\n/) + let logLines = text.split(/\r?\n/) // remove last log if empty if (logLines.length && !logLines[logLines.length - 1]) logLines = logLines.slice(0, -1) + + // JSON parse log lines this.logs = logLines.map(t => { if (!t) { hasFailures = true @@ -118,7 +124,7 @@ class DailyLog { // Rewrite log file to remove errors if (hasFailures) { - var newLogLines = this.logs.map(l => JSON.stringify(l)).join('\n') + '\n' + const newLogLines = this.logs.map(l => JSON.stringify(l)).join('\n') + '\n' await fs.writeFile(this.fullPath, newLogLines) console.log('Re-Saved log file to remove bad lines') } diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index a2688b88..3deb4030 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -327,6 +327,7 @@ class ApiRouter { this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this)) this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this)) this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this)) + this.router.get('/logger-data', MiscController.getLoggerData.bind(this)) } // diff --git a/server/scanner/LibraryScan.js b/server/scanner/LibraryScan.js index 1dc945fb..ddf3c66b 100644 --- a/server/scanner/LibraryScan.js +++ b/server/scanner/LibraryScan.js @@ -134,10 +134,13 @@ class LibraryScan { } async saveLog() { - await Logger.logManager.ensureScanLogDir() + const scanLogDir = Path.join(global.MetadataPath, 'logs', 'scans') - const logDir = Path.join(global.MetadataPath, 'logs', 'scans') - const outputPath = Path.join(logDir, this.logFilename) + if (!(await fs.pathExists(scanLogDir))) { + await fs.mkdir(scanLogDir) + } + + const outputPath = Path.join(scanLogDir, this.logFilename) const logLines = [JSON.stringify(this.toJSON())] this.logs.forEach(l => { logLines.push(JSON.stringify(l)) From a9e9808183fe729944dcc880c4b68f7246ed2e61 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 15 Feb 2024 17:05:48 -0600 Subject: [PATCH 0361/2145] Fix:trim whitespace from asin for chapter lookup #2605 --- client/pages/audiobook/_id/chapters.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/audiobook/_id/chapters.vue b/client/pages/audiobook/_id/chapters.vue index cf48ab64..0f5db77a 100644 --- a/client/pages/audiobook/_id/chapters.vue +++ b/client/pages/audiobook/_id/chapters.vue @@ -142,7 +142,7 @@ </template> <div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative"> <div v-if="!chapterData" class="flex p-20"> - <ui-text-input-with-label v-model="asinInput" label="ASIN" /> + <ui-text-input-with-label v-model.trim="asinInput" label="ASIN" /> <ui-dropdown v-model="regionInput" :label="$strings.LabelRegion" small :items="audibleRegions" class="w-32 mx-1" /> <ui-btn small color="primary" class="mt-5" @click="findChapters">{{ $strings.ButtonSearch }}</ui-btn> </div> From 14e31d56909556092fd95b9e5cd7382c6cfeb24b Mon Sep 17 00:00:00 2001 From: ipcintron <ipcintron1@gmail.com> Date: Fri, 16 Feb 2024 01:32:04 -0600 Subject: [PATCH 0362/2145] update pwa icon --- client/nuxt.config.js | 2 +- client/static/icon256.png | Bin 0 -> 33633 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 client/static/icon256.png diff --git a/client/nuxt.config.js b/client/nuxt.config.js index 1736e3f6..7205b0af 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -110,7 +110,7 @@ module.exports = { sizes: 'any' }, { - src: (process.env.ROUTER_BASE_PATH || '') + '/icon192.png', + src: (process.env.ROUTER_BASE_PATH || '') + '/icon256.png', type: 'image/png', sizes: 'any' } diff --git a/client/static/icon256.png b/client/static/icon256.png new file mode 100644 index 0000000000000000000000000000000000000000..4deb595b7632e2fe0ff30774a009b6a08e6f38bf GIT binary patch literal 33633 zcmYIwdpwix|G!dsms3$u#wsK^C1GQxQVt1~a>x)uIgVkDlf-hGQ(20skjj~IJ`cmN zoQW_q!<^>0&DhW9kMHC6$Mv|b>%OnYb-y0>^|<cm^YwbZt~=&|se#~$^Cx(Ccm$0M z?>yq+IdsrG#Pi>=g9_&R-Q|Bh#3O^-JVm{Lg@eu=XDcHY6BC}R2krlO4xwN29Qogn zgL3|$@bK_{KE%U&P#^l=wa<tD@9rU<&%FP?{l9^(i|37acy96--MRHR@X%5{-!IP* ze_W;gt<?)}g+zFaULBE75iv?W`@`{Jj$urNvr&M@-DChE#sHFG<@?Y>&h#|6s7&V) zkx`=Vct*@xO1kVvyYksm%j9Q=j=$nD+MJ9)uTK&=BTuI$x#`Gd1SxEF5)o52S?k)% zB8B%>5C5eumG3}zmsp#PK^x>Rzwuv|$0GeVeQP&7hoy(i$GgOb{}KM;r9*U57=@2s z+9LksQ$EnEr--35UBzkGJr^JK#Rb;S>ai?z?;F)GW4f;HGYA3xY_#?q#BV4<&nsu= z5EPb}+TeB{le(M$8N}z+Je!{sn`uR6GI6kWB6Q5nuKd!T<&5puolA3}=Ll(!iHM5; zQ)k6+XUJ@1=EjMgiPi{Fxfqol>2aw@VfidSb#}^@hq+~7ak9h$fKXQi8rueO>}s*@ zjiQEs@M}GtcK3zNL)-raWIlanxMruk)58+R-tm+__5+JlHcO-**pJ$#o*yL!j$u80 zJ>CV!O{ihsgJ$(ncjq|g2>MrpO-aAg7{O42uOvyZxJzqa3Wy_orRdEu!H|uPk<{6n zS_IV*EX^J^Ji8IN2|9doZ2XtQ<I|1s5vu>V|Bg{!kWdK@7DWRrxJ~l;k#$zCLsj7& zs~Rj#Vxjo&jO?#_4f2&6ZMitY7wH%xY$v-G@>ey<y&M{6pj7o~D}6$$_Wmjhklk?Y zX?vY_Z}Q4sW?ZD)aaUzI;?><!9Wj>7(Az?bFx#`0$&F;`o4PNR(?qM6Ymkbx4rnEo z`ice^3Gtsu#4It?<|dpQx<#=XG(@PW9NL&x^jcevjw5b4*Da;iE1rb{W`FvmS1O{O z*sly^=va=^pPTl+uTIa}8|!72l*<Uk=#HOJ0Uip@5(M435UOT`sV2#;kg9cDOi5dv zSf8b(ojSNoCVEArh%F$bA+}oOSf?65qH4hi9k&z#oj4VO2#pEDWRRAt1KG`h&Tp6f zAsY1b#(O5T2YMnI8H0!O><?C=H8wkKIo33gwCdk#uVFAtOsO;E%(~pim=nTuP~gMx z6uJ!Tt_ZqYb=q)8TO&`~zGkDWn3hG+#^7XG&7}y*oos0x7i$0{5h1BtMnia*%Gvpz zb4@SC;#oRemn{p1YcEWaMn(0EVQN>Z>u{9xRcPV@V<O9+X~-Gt4cvCM;nW||k<Lnw zaCMnp(BvI2<DFl&#-Yqvo01GIELY3D&^=~l?!(#MLu==n`BpOVD)(!4|41Smd|m8& zBeOtl-9EF#LNOH#t)rTXysx1|=-k%VA>^bALp30T9~Xh_>$+f>PGbOcMvX5#Ej}`I zC5w1h07i>2a=|(208vK2GmzHtRVxPA9IzY(-b8*)Yj|Z`^oU6LajSr~Ueg+gBAn&} zP9PrU(L}w;fF?l9n-!#FV-BT=#L9@o$P|wkQ4D7EUK_u@7y>2yAxk&QhFA$t#nbk2 zE2FSIC=I^ZH;fY2sBib`-8#>*J7HY;IaomGnheNS2pitZx3c%~+I34X8sDNa?$6OV zE_|F%BHd?6Q4QEgL1@8@Xdny}0em&NxE@RK4qtb`OCS3o0?(o8XRLL9+K4chCIku6 zw3KZdoJ-*{FH0ld+#7BK%;G;GD8)|{_yvAw4Vf#<0hl3y3G<CwS)6BfHht4LdBlVq zij-AaI0~0Ve9D)T^64iVS<(~ogTH|iky9|pUiMk_rWhM_6`xt@hHdA+$)uqlbqhlq z5iTBP+K2z+&VJ?bU6(hxOV4*)bh(&#o_X4avG^m@;E$Ml5?ZmfEzHCG=1)aFr=%j( zDYSPi?gs0Hl)c(uM!N0t@h6Rdjhe3d(bcv-lTt_1uwHP7-jh`YE9xMpKJaRsrA@+{ zxYlvXbY$%)ck$mfyL!R9JgW7m4oit_2UCSuT@}l(AGDX+=H>auq6YV0JvNeL(cGUD z+qp}l3*^m)CPwBfGH!h+x4XY=-dmn*G)44VUFd0Y6dIZkek^omF@6x5Fed(pel;UW z!ts1A!2fC~HOY>d;N-4%t6loZ%)GddVd?vkpt7UZ_bcKQSYp0caCIlV*l(%lXMS1q zYczh2dODh5b7bOzSBJ`Bkg6>C5)z=0F*tShIwm_lk1Dlw*0@MyVEr1ZpBB0q(t1%p zv>ARSYya!Ryr*%+vo?3|ZhJo5@0vi>+~@gEwEG}~Sfj?A7WIjV+hW*6?H|i2kO=T^ znHD+u@JsPwK6TW9w=8kQ*Ccb*zO(C<5GN1H+sg1Sd3K%tsxPOlXRu4GrfH?}e`w4S z6`8f0_$!TeK{F-8+G`HkfaptJD^mgp97m=LEB9ARB?<{2#~d+uEv<(047BcLG$qIC z83Y$Oe<)<i<`A}E{p1S_C)Wj4c)3hFBfGh7t_;Hb_--_f_zH?~-E?(%W~<i3uJf%| z;w#8t85s@w4d(NFfxnxUW=MYP=xrus)Q}K{qz0UlRCP}NRv>(W7MxH}+P|P#W$YV4 zg_OO7H$#f)y=aAQN?#P-rvU9bnNc{JVLV!vhG;%h1T%zp0(kk-tE<h{I;0yAO=@!i zm}C>Xg&Ws=UrWCq2pe1j(W>mhCAp#$E%E-a{ty$GI>|P9;e9APGf}u=ytmk%PblYf zKmX((!i?Ys=}enkU2;3g`SFeF2zTfY2`Kl0u$21q%8!Cq1xEN!W<)hS)s5Nl@p{WR z-+E7f<0*ele0#+F{_EU8hkrqF5dKmJ#qMMd|C@HRtsm*T>NAhN=Z3acPP`L$kIM>u z4KeVB%*wNLaDTfF{n+~QA-EKJ=Dr53t?#X|s?*eK%enZ#=W+J)$PHDmnIu`6#dy`( z6V^4u?2*>0rIItc%DQd+7m^G>;sCdF_nhes=2==;Ud76(@7O~+yptl|<NNfv-*42h z?p{$YC&*g<7+Ph_Xnab?m1`D&yVBj|E%EKwaX{PBzSxDZ?{M2sMki5&msDG2D~3+{ zz@!z$+>DMOk4U#8G(s<$<x_q^>^F1a-*4R<{q6)ja>I_H9x^y2y#A)<6)|sa|MvH_ z(MmVvI*hvm#e4b~)Umgd)$<LA8%KbiWH#$#7;}oyxx!PO)ej6*XTYxaVPQ^B$r`ST z{Elr3386D{FbA=>kj}*Fi>}7D?H3;P6dK1-rEengUf=Ensh<74gdE>$`|egf<sYP3 zuEqx~v-n_F`C5BqOWtN^?P63-L#(ZuXXqBB2;co-yhw$P{|Rw(NvAMK<ofp&gq~3g zk19j<ulv0|YAf4UYuvQF&y0hKj^!ju4oQ9HG8o4@A63$^b}D+gY{mTQ>jPY*U1)Tg z3Cajvo5f2ZTMrkWQt0r{j%U@aT=yck@mD@cAyS%s?VD-O6Q}TRNqc@pAVgRfDCjK- zMQxF@6%;BxQ&@};&zOAjay``&wq7MH(D>Y&MQS@~AW4$Ts+8W6=iACv$-gVe?0r&D zxr9GUpxEv(do~%+SEYos>eIwC&gKbA5eWuj5lR3hhg5@7B|^*X)~Sh$?aB#77D)1c zu6|b?V;=$ya6^y#Pd{oekFXg?aFVY+E@MxLzpc*ibB7(<mRfc)%0L1kUL4#q0y<eP zSln^8@qr$u>D=nEELp2H>wiaK93K6GA;prm)33)*n4h#VzKQdJ9XegWh%LgNwp?5| zYIpI(chBxqUGpNA#~yuKn7w)~*vD7AakkCNx8O<2V^s4yGr({CtFHse-!!{G(6)Gn zd8{gyaCHb~Qzy(bY$f>F2%b|biLeD!_LXnK5L&9tazW(<s9(;;TopW6RY#F_u|du< zVU=+`&9hjO_6e_}LC<dok^HH|ZNM-d;ZboWTLZll3Qta_RE6Fk*kXYaMgsjlBbCG{ zBJXhWC)APot%pIQK}KjNZN+A30~8a^|0R+Jm~mDdpY;V3P8Fr?KI}^<?p>wAqRDz! zwQG!v7ZKDdztBk}l`#i}gH~)TrYOOxD_CVA*4oA9kg$^kpAkk-*fpaA0svE@9g=Qm z6}d`P$dvZi2NhPsFeHW=IvBIFctR9uZ8sAP&P(Jexi9kU&-d-nbBOURRii8CNEKD* zh!r%wEJ`=I)p;Rwgxupuo<7+Fp0`dh6UlO#KcKOJ${~PjIU9ZD-szcPV1f&Zp(Z!3 zgL(ad1{2jL1lf(K6}yJ?E@cs^%)WWM@eM_~!JHBt4-PvrzwCBB4XK6`%yR12;j1zB zd9kPjy@q8bF`wtNUP$VpBbPZT7wl$<pwOAjxRo$u!)~8!y-=uclfvFx?7H*XF%hgR zIOe^Kedhqcj#xb_I+sy6ADerY^%m=GEiHs0P<SI#Wg+DH8YaU5kNh+-=qI05g>k4g z(-P#Cou8S@c$U)I>+}7P7|z2xdbRkdbeRl6`kW`kc;G75)-%7wI;ZOwQ$$=v$&z2` z#!R`QDRcAIGrEBw&P{Ps0hFsx7c0LbNEP4xyPm+1^{KO35=WCZB8`+gjqCeWhJJBm zqiCCuud&7Y_vb8Dt2{y#2bE4!V*qFnS1OoE-=V&@5j5)2DJ?!;4_o=K9=>)9TL|+F z*gUJ=3zoeDw26~$n0}UUXaACoNQr79<?#D#^+bh}4e(zjrQBEv)}-*7VbjD}JEFRy zk_!xZTJvnoGiIT8t`+`B0<7)KXQRYayk}ynvOT=LEKLEzFX%NJC;jStv%86TW7^LP z7Fi!(Ep<fwN-ipM*X*1hxIgjroqCw0NS3c<y<nk${h4{ELX0h95N7Vt$hAw{SDkD7 zZ|e&tZ~Itg!;qBM5@OkA8NE~4Zl)gE>o%p6P0MV5ykS!9q>$Jq4i=Ua2)Y7TL~zED z-6cq_4<5QFiMY+l1TE>zF&#*gVfe{<^0=iik1#OQ5#%?|5DA}W@Zy^ZKun`Sw4ND6 z3lQIk=L4#M)y+e;my{U(a$lf-<`qOjQ#Q3#+~@cjF9ln*g{^#>fV!J=W?h4n*z@#X z@ol`hS+VKYDw`M<o!2wn$gZ$B@be*!t!#Y{7nQkzj34r8&{?q35eZ5(g2(TSHLpd8 z>@lO$8X^<at5A^POlf)RR4SaRXW-I6DLR1VW!0j%#7rto`RI!?I-wwq?a}F>?G;@V zXs&f2nqmqK0g*TP(UdJ@7Z_hEryBy=ZdF5v5i$W1yQR*qC7J;zh=0F~j{YVCwV0#u zwLU&HL8>7naH&~4npb#215mZoPEKZqY&MUh3BYQVK)!odmmh2|jfuHS=09s}nGg>N z6gIb5E1<>p+O&;Oier_Cx^qb)18RT4-^a!p&x}{k8|EVG)8c4{PT$2%7PUxE6kn*! zrRkfJt<lC9I|#rr`4f3Pj-H^^@~8gW218U>*mN7Ox6PS-g|~vu(A&_(UTbEjhxbQ6 zC+co-$T_aM;UH>;LUd}@-fMD|IY|-&-v1?TudF{aloN20VtOe+N-l$u(bfd&59Bn3 z;j?$9v><^5bv$on*vr6qj60flX9@y2r7iGVC&cM@tYpjRdkE66&*vyR^qMf7nnwk} zWl$czvz&GBc3FeD2=B{365i-yg(qE5)O1Qm9)yW7_2`3gRHm*;Zd|!`-e>4(w`_bM z<NE-9I)wyYkw#xrrzveb*>(%9nsmJ;^ZUC>MuRaS%iqI@3Gbx(OVZm;C@2Zhh-4%n z04TOPpO>-wmAo0(;0fBQ!wM0y0``A>X_=D!ksZyu^Y6w)_y!Ff;?3z^Ybvx9Ol&_D z3B|n2G}^T9A@p#%{m&_*=FBt@@!n^Qbk?_hn=S%j9x`DJKx?o7Dzw#D89y;H5Y-o~ zKIb?sqEJoK<C-n;RQw1cd){Mh8jG%$upvd?681Ow8~g)@bBiTSb-1~BRk>DePU`0c zO7otkJ$S-@4~^dS;^5aAxBd+Wp%+S~jjko--4d(x=Ucn+p;%Jl>!i|FbqKdqPB5>n zeSeAA=nbNPi(?O_mCUckv?l8MuEI&0Q1Hb5`<-{0Eux9g)3i&NguNyqoj85D-vp8r zU7Va++l~|7i(8E|PY8I>e53~EAKz83sJ#2nLa^;;gr$35nwemcAJg!fQ6K_JqSZLo zpK6RRk5{qBed>5ZYSMjZ`d4SSY*n%sMV5$8?G0{l4epv3{ui_iAIDDVOayI(5DkuG zD*QR#>ce`O6NC!c5Sj~JP;`gSjfc&OeH)JZfnzWJ1{lZzgDhs<O{VnXX-}h*KYeU9 zngi`rPdfS!8Dc|sLLX?qdVykMFfx;#en!757A){~<a58)tpE5d^vYGjNx~~12s2{e zF5C6;&!PN`&R5r^(O%0P$2nbBUb$#nroGt;pGg-Jrr9xgOUdC=@FgSH&aHRei8_8X z_%3!zZGxR!DJVR(_q$B=iky(=EY!7IXXEO5qA>7FYjwiP1b7l==Nkyv>kJ}a>RhPU z%!q?vv`IcAWW)aNMy6!&w`WT{|Ap$WED5}G-zp{3m)I<oy;M9{g}r~vbt?;FVkziV zyC{k8u{>DuS)BK~d_L;iX?f_mdO@GcXo%FIB?~^BhCe0ASaWEe%{i*VO(tiEai75| zw6|WWah!R>I8A$aPkT;1`5S591?=!$0^6Q=n0UBxEUZp2TyzIP;tMZD9T|m(e*X9F zsrN|~?4k7@g+J7KJ0RlR20NCJxS0Wu+G1a>6O)4}?Lx9R^%>yyjcNBsay#2?l7X&` z^CtMa#msUaK`yK`>*dR1Lap^jHAC4W=<(uU*Cj>a)+dop3XxPY{i}*7$I%t=$@!8{ zjj^MS*oWs{QpSz2^Y+Ne_EkScjhU#g(#>x#SfJFG+=c?-v`xQ2r2rDJRj7%6LaxY1 zznRICwxyS0XXksN4IydsNNK07dS2Q6aab-&#UN3xyBa4}hn5!t*j|Uu64Lhw=mC{b z7QEA)i>^X(Qw|qz+^yVPd|(sK2Mhtt@=})T(KBbCss9tHlT+?n)}E_kf_%G5R!2pN z1d*^CeM+r#ubrv&C^B1^`)I;^ZOj)KQ6&BXH58h@JqOu!ADr<$Dh(wrA#G|#I-nkb zDU(PJ@mgZkYQNJ$2*ZE?5;FvL-o0Nz6lzM<&Gse<0D@;qegEx)i#era4t5V-z4==w zeb@T&YSUijI~#vs!!8}KhqMK{{`FR=T!*5jBYZ<v$sm!wPC~zx`STZ^F=8>kHr^8& z1^Bf+Le?WMS?J2Ua&#)r-v{hq?WGRgs{0*Jn<MSf$P!uXEnnIfFK5&@Pp&}0FW%{t z=51{FqO+oXR}6_=<Fv|}+R!D)*3*_dF8CiMlS913SK=hRRvKCSJ~k`Asr`RN8Op_? z30pQ{j5bMI?-8n$=VU$6Kq6Rd^$fMqZZnfNtnW(0v(q&JLc5gto0?ZL0hJfCg7=NP zbok|PJ`ceWl^2IJc!dA4hRA<~IWusQU=}7jLb5LWnFO2t{C+8om&@5g`8}}A5RdWR zoKJCcrn&(<KxJg3ojITWOqvFyt!075c!&o-GxQDa1{@>oA!^ns0~db_<*9_WWu3$A z{sgMnH+<O-Fsvu)!pEQBGe^R5CLDI)8Q_rwbvKH-{{kaVHW#Iuc8!3#mq&IL7&~ib z#c)SosI_f*=gswzYZFVtsb%;iuUK&~Os|%>ioN-p_NHvM9QPlblE*a<&H0Ha`0KXW z<Ght)xy<>?jw2A_m8Z&JYpoCIW6ZTwvw&^q`x++IG^WnfRTx`)xI>!<K<X&$d8N@y zG1GCaTG|Tt1Z~WWcqj#mZtEfKG$&A7yV12pyKtB$H<%@053MX-6Rb0N1gy8wj#}@z z<N1NmMtDO@v%!)YIEn3qXyZg^qjS|RJ5kWGaVM+IAk(HYOwP#}XR#P9{VH#0zVc;> zmQz^!-C}?;x1rIl0aGja(w{nAMAwB*u-m32SLzqK$T3WSz1eVECW5Mi(*Egz#EhP6 z?TeVq_1~Y~<hRqx*qNymaJ5@s^kp_k4sH6+7T3{eM1$914E1@}zkNMT=DvKNv(+RS z%YkvX`JD80xHkLDHO}fjaoo3^6GM=S-JJ^$ucB@=YD3_=Lq6Rdnv27#pFJ~5KS<af ziXmNQlunnC>#%jsAlmu};Y@TYrsi#t2W&SYTy=uOnv&XHn4NxlcKac6_tW)e+z$7) z4quaj%v?%BN3K4P3LRQwC&WzY`k}A<{2(+&gDYnMQau9^{j7OI$C6}z<`)q7ELRz| zWtP^MZF^j}JvGoPad*}`{*!}Lyr{iaP@XsG_m8iB$%~2SiQk^aRqU22H_$FfK{n3m zs;;$dh{I_yh$@KZ{DTZo=p@`>;z;hQ_I$(JY`0{lJCOT(-O|)&CBZoAVO&G-N_@46 z{W)WmUe>y|ONh`WfLA$&B)dPM0%B^Gcd9H@pU%@*;rYqxre9Ti&|606Y#{E>+sm(d z<hLqPCRjtlZLu70;?6wL8Km=oB0uU!y^TP+K}M#li}z#51+@B=?*>6cF%+_j9oBd< zDC@1D5ik+=(k4B^cJ$D^C&efL<gWJfjFr7>7+C`40@4<GnRwuUPHqABsN~*a6llA5 z`2qKZ$jZH?VQe1s{QQ5kgGmFbW)nF`pC6kk*^AZC>tU>aT+oVtU;*qW<mYp<^69{Q z#?dRb34*7Afm;^gB!i4VaI{q@0!Ehl<k?2E@|Y<r{NUW<{#p8r<;H@oKC-=j-h@kk zSReB8?$DQgO(+XEGZws)MvMwMRv$v$&)>d&e?MXGv{u`bIMr@S>tLzK6WK7UJ95UT znLoCs_g`)$JE76qMM2>c%RAL%a51}dz3-;>u=LBnr|9H7%X-S{dvv-osvO<`l%NR} z1z)sPBB=y!nW6#>IiEV8ux5U2En1Q`|59El4Ug}nde_XlvH;Dr*YbT|+g%mE%qxqH zsj5GWBT3@MRs9l|+qME@FM5+!gE|_YK-K;DGFLFQGd<z7?;5CjdicU}xMstl9UE#9 z1lO3iKVUBt_D$GJ&8|whP1+t4!G}pWf<66BUl}{3#{KBjNjdfRfqw$GsHzTWKd~RO z2*wgJ(XJq8=e<_7EK#haN=%3h6lMzbt*;%SL54ld2MN<wnyx2DTd!-5+mYNsrt!wK z6C@4T`n1^OGD2}GEL=H&=MrRK&h;|@N9baI)!Z*6kEt+Mf@Ut`KIw9`skVz>w2O;Z z*$r<rV#2y=oP>T!|Do287o);M_1522xxrbd1TD2AL`@r?RVhFpNyeuBDvbd=#O0Yk z&^a8al@J)`q=d9=E3zG7lImv?Y~qXj!j2or{oD+)&k7G3nYwqqz83P{(lwJa=q)n; zX5LBopH9Xp`_Eb_LH!S#d>GHpNFFah?QU44wVKCiukdizqRt53*QkkJt)MeyhC*}i z?m*r@v9JF|n-GvAxUk67iPlcDsmrGIX<-ZQ+!lCW<=z$f3K?KEt)0BlxZ5a<Y_0tS z&0DOgN;K*$p@|F-687>>wNgky-x&9aUTXKj(-%4wYzjw;7k?CQq625%M)BJ<RG!p+ zy*pDYxYIs$&PTdWRnB8C{diu+-fIwKQhQ=+%dFjxJ{I}-^UT+r@VNQs)w*<1cb~me zI2xZ<@1aC-BcB({FesH5DOF><n2g|nPUeK^vi2CKp7BaUq{kmMtG$T)Nb$YDoyr5N zNIWfRmxc>B-4O>363ef6<^SLdgl(?tf+5`6eaBZv0?VlEdd)BtljF_x#z$7#o1>D^ zb^H%=G9_F|D)FcgSfa7&!N&<BnrPO)p$*L=QL`HnU?ndzm}A&1?o+gOG4}=J#9kEj z;#CB74*oBSIf_4E+c7Jj0fbwU!2HBhjUu&^3U7d5!Xa0oI;jTm7*VNv+$AUk{4_M| zhJs|u(!yyx`d=zeZy%7lH_L-;ps$tr^=7Q7!ez?6KA4*7&}S@#RhYO&Kr4iTD=D7x zSq2JNHf(tdB~BoX!)SRH-)wr8t_G0Ilw~4xigy3l1`EyDY9Y^rDQ7u$juTU7{C?$B z*RB_ayH`>E3U6OZbMo=JMKmR@{h=h9#wBTO9{uFuRg0ODMdj@Np;F97uPdu6hlH+Q z)Fh4*``iX4Ux{%p57Uu-X-N2)-gN;+Q!g{4DAh<Mm_iy%K$gFnZQS~a6g=RjjW!KG zG<U!O+U)c$DD{5NKT$p@Km)TPOTw#u@t=)UR?*+O`GoLnMWUYB;MCJm-;}(-COeBe zMkDH8m5iNha~)tr4Xr1*mx7`W7-ibo1+MpLP@0E~8XjWLPI)M3vRm83WfE6vSp1Ee zu&v&3E0w!#w6Tu|0Jqb(+t^V+@$on(hC{%xz}hRzQ6TIZKw1iT9Z?d3x|Tyb*!-c4 zHrGJUx{u~P%NFAd#+t~#U%Aq;>mIyBQ>EG4|9DCn(#DM+?LVA{Jr^c6h<tFwwguVx z7GAACt1cr(I`rRy<Z_uU8Glx{k`v^lX2VENMYJ56x)<hH7wxCM{=rOh0it5c&fJ;N zeA0W?&YRV*jaF%-(}=MarqJXpz~KALu$BVgbz@#V7uW%O2?3348FL>&WEIyw@{NKI zvrSm{nnoJkp&lmvZM6+}b%#F#{x^uKeYDCVSuO*GI!Ce{3BS}}-{)2SO*qAEbIfx@ z!9BQ*uRo5UDfd2GY=pQBqvG|oqLtHaepUtw-F$v(9ca{48j^CDsZriW1UTA03Q=3h znDX&WZe(@Q_Koe-Ahh@4$4Fi4+wRb7o*bWJHQJXKgYD~X3-jr<AVboXv$#QZl%hCM z4eU)qY12i%N1z^m2d`}D@<vzwxNqg`8o}M4$bAZV+kOA@Yn#BZ6$S1R+(OYtiPAqG zDN_URs<nH)bQCCJS<xKw2=?(nU~cm%8ppV124a6qR%Gz5)y#&M85P0^xXl9c`UrP0 ztyp&|gz_Gbo$M3Z{msxF2zY*M(;1kVb?`d(Zw2YMSj5hUzD_GlD^1%{E9|{#Z8LUj z)`%e}A*q}qz68cmVj~)3S4zPc_H1WBqHy$MSE07BeJqNQ1>6j+E&PRE-EUuus$6(u z&$$u6>bB4BOgO!$72s@E+}p`+U8KEEeAC$7E|+f8UV^wUOaf4&(U@h}wF7MhQBb{q zJLCD#gm>=CF*~*-*VB3!tQ%B!mYtzO0n;X|G7N-d)I&<8_=?e6j^x_M9$~d^r)q9} z=0s)RbTNP7pyaiGO5v6|;jMg$I`u5(G;H(h2cbzOzv|$HZ$6#ox9jwY4_lgSJQkeh z`GnhL8$pn|>0Iw*SE_|YYbCe7Dq$<l!K@Dd=bqBJLJc=YCG<3AL0`!yo<Ohz#a?t6 zh!b>q1ipvUPZIasA&U)z)?$?ks7h6v!}?Np$nz#=BcxKq8E4mf;BE~0;<=}Y{isUr zUJb=F%=#{DtzXwdS#VJ*vH!8!Qw1G)o{MpA8qY7V<!W!dOFD0_+Wh(&W{l&TSl6yJ zI&Ziioml-?+^gg3M<wLTXrq7Hmy_;BzFY2YUhN0BbiVfqRqhLcv0lu+TZR$Hm(+^M zXbR3phr54tYD+gQr2mWe<t-LdVRf&KsWDC?K8cL-nL|AzZ93>Fwq>daNF2W`p=j(g zm|>pWHqtY*7NSh(sJMD;Y6G0+Pm-u98l2A_v7FqAE4@{%#;5$3J=x6Pu&dHGkYI*M zJj`ymc_V7W^3~>b?4;8l@}>7DBr#6&D~S6wPq!_9HEf5r)b+r-oSQ;}98|p+w<AO2 zI=<fZ=v`3st?~;~PMhfM(E=2sF^S?mF}-YiW*P9jDR`H)GWocXQO<R)l9w9)t%<rH zso&@vVlu*THd35;A_JawH2#@9;7@33PHK7iEXA5nFF4+5-a5ANN&NYgkGr3WIRBV# zTQ-uDk4MTH)~LFVrHU4xZ{1beIX+&Ue&8h~>XEJ!oEyh`&tPod1s<Z0;rMW~f$b-f za*iYkEI_Vdr8Eggkqb5J4m-Y-)b1K5DPz+e`tF`whK~cJCB)rzZ?$oG@xVvh?+<J( zNg80^F<~bM>4cMqosCBe9z(rHt1*-r3Ll@IOL(H3mscxzr4(bl%F@7pKV-H#KZ07b zCs6t+tQeaYPv_*?%SNh!zE?Bq>L0Hq^)jlKkXZynWEMRgTkRF1<LB!@3uV&1m07{F z!G&M#Ov5o>jDMcuZ~QtyzpkrF1et&Pp}WRNHqd}#nR;$~@XmZLT`7#uRyc>V9<p!E zdD_Ff9Cz75ZYSdD=F#z7LKE>(=wFyed(5q?4TM8@Gf5eg45TsyRw89zI@xg)BS`Sv zyzC$JfH?Eh>he>Kd<`As^o%!`Q6}_Gd_9FnLLR0iD5+bq+nkZ=&mdE+s;1$Y!hFZk z;#JJ#1Wl<!@d|v$GYqeXj)H?oW>U0Z2&$yVx0|7cqdQb;{oTm&-KOG$EJGVDCm(bF z%AuU9<ZzDWsGnW#U-PtN9fY$*e9F{Fp0q8;W{5BYQHoGqi&<qf%~cWM?ElcP>CrKj z+JRq+E7fEKegw*rkX(iM?Z-9h4o47YoSy9^w6=7sAJ6-rc@z{momVU-3*;fZ%sg9R z>1!ln7Vi`z*Hy*)s_}k`yPA;y^~As&PLcK$qwAwxCf45y_X)N#aQri^VZHHh|G-QC z4?o_=PaOm)X{5afULi4n#26*wVy1$vXL|AVMfW+fXi?f8hQx}Aun`$E7M6?7KwKv{ z*1l_yGd<x(D@b%bczNko^qci}11X~msp}y+q$xaES|^b%#XlnJ-&B@K38`*QYE4ow zJ!~4un~@5xB1qdO@<%_eqvJ?E3;VRX0=%fH5Vqf6t@9<V{sm|!D*OhT^tUExc2Ss< zU$wM(v%qtpcxZd^z07Gr{Ggx~R7SExqbg@N$5lcZid{*?2<z-hX65S%z<e)-fK+EQ zQk^_brul3>yh)t<Xr*sc-A9`cLwR%DHu%x_Qqw=anNrH8b71{=nYw}<4~I$Lg7KN^ zWi9Bn*~3!Pzb+HAFJBHHTOrTP9#b)90Hk|M!S#frdcAG0<K%QE_f%rbo2Z8*TfHuM z?{$Sxb{Bw&NG^zO1~pQb(W=-eTbYoW+UtLb%R`f~q>Y+{$&C;WzzihVK=T!lM5Qsy zPn&(Hlp=7RY9QP9_AA4vVbRK#zl9)jI93Tc<#S8$*1RY*@Llt&227>_5ciMb4D^SI zpmR*;UZFF*j&$8RePjK5%1NQpj^q!g6E{Zh&_C8sJnQiKoaUqexV<VQr$eu1D5y`V z;V1v2M`zKY>+X!bIELuJ9k(CW*6mM4vx`rmk?Bgn@HN>Gan$;jj5rt;{67=J*FwN$ ziQ?E^A03dhGPy=r68I<(!Z)1{WDP0(yO865izEsO{o{4YqIaSxX+7K6MY0y_dIk!3 zu98^Nv-sSUn8&hZYQd>WE}}8v>-p6QIA>?mi6<OzyZfmu@U${{Ge?L^3h(?x?9tT; z7p_$;QrCrtaGsh$tfRHoN|Iapl65??y<_Rr%X|%Bn;=B?@YR%2DX}c$z?PiaJLrK_ z_`PQ#7lomTbcf!N@}t12y}#sk1xxN?iPw?ZX&cn%&>IEg?KjYa--?X=K!$LcDq~h% zC)Q>Te@hEk<kU~D@$(8YM#$YVz4WsEEq?Vn;-6eSjbx<~J~vy38?U~bSp05rofKpf zSVeP6oX`xVs5M#_u|mQj$YNnkPzGi5x8uNxKSUFuP=p3JsP|AE4B2}*oMa#BGm^=T zl)K*}tCfL!Bi*S>N_Cpvfrp07$PNAK{<UP`NwZ}{mwC+xophTN^T1kTO|pT8aO|u> z7aG$Y3Nj2#xj|?uf1^-+z_EfjX7Tbn`dOSCGjmJvc)7-LYfD9lU**9rax&J}7Dw*s z&{MpDrl94C69sEOwR@y$cB2D#Mud@fi2e#%J%WJ1O=nXPvyAa)tzItXsepPADFVmB z@%=*tW$fw*2hX(-rilE<#KxUE;z&7|!%{VL37@>VMT0}Zy|0E7qU87<X4PCC``*6X z7Ai4(iay@4Gr>G3THO=^3vC6-5q3aIC<cHh!esF*y(QIy{UIN8RXL@*KS!>XDJX{3 zZEHdnOb5qkuqAGNy|HwxN#b9GSnPAo%Qg^KHeYw15@P^7@5Px|O+$ujEwS#^AO5BF z&}ww_w)e_Wd0poBlOq79m2S^Y{)7*$@}<#Aqo>rOzwp19o);bDbi8tle7DykF-N&t zV)s;m6(4605*%_D3kSs(8!fbXiP!>1el7hHu=J!6^q&2~#P6A#o_KS_K1m5|wQ|4t zecoOSMj06TBO3<~jZ(4ul`PWrTKSW1U_0Yumzv!%nHb|!zNuN!D<Zal1<t9|+zm$1 z{cR__Ppz1)qYs7DNva71ZG6S@dS3OeW%J@Akzs|B`y-{BujhFiCN$wW8<}*r3vA~A z&eDwBVj@j(j!sr0v@f4Rx(A30;;NY<628xsRXdjQFkYun*HEwS65&Gua|k=T;Zy%E z-jjM>L<9em`^U{NP$o>zNMk9?BzTGJ5TMwQ`+g!NbKvjZXF2WQ`DszSHMYTpDiQAY zdper5*E{es7Pk}^=)|Q=zLJ=^@@#1Lu%A=X@To!tGi(L(R1;G0<Dmd~Os!BJS~YUE zqk{067CI{RX~P*aKAS+1vR&1om95GQd|RKiX*f-K$9)9b@g}{K6H6ZL@cPHYF#hRa z=}rOsZx=}#3DI7I`E-mEi49LY5)Ja873+UG<Rv82!L8MPlF;g;G&2&nc6CmreO1Vg zx|GWXt1x@lpHLV+FEZ<75BWdMa$!xnCZ-S6BYPRBDjc|#-x9xC<3N>}4!oVc=6p1p zBU~7^1~=__64t_!%K&w*%RPj?yAK`X@cX$9dPEFv!!BV1sTaHtQC~EC{jb1eXD^FW zKoiJoOT1t3*K5B;n?jW3dtH>LJZ!<9SPbSGKo+lSB%a&SSaGzmkGYu0arQWJ_9p(* zezWz5$g`Cv^Qjl=HdH>J%A~%V?;39^WjsjTe}?^V!*zWDSU7*(Cj8!g^%HG~W_j=> z;g=oYIi-ai0kc-`!xNir$VYDGK~E>n1oo$slnex&8lS%NSUd}?VmoLxwCwo&vF^C2 zhUKVxI8C;w;ETqTh6XE#fmx|X>$mhqTf%t!^o>G-m8wgxWj2|GlA`0@MEXH!of)_( z3Yng?5+B##Pu1pbE|+OHu>02E0eua(|6T_G!EZ?#pzDM!Cj4|{xDX=ae5->BWm-=p z^2n;uwJK#QKFgnReNH1N&0&4vzZHzT?$ZWU!|v-pr0-_(|Jl3bHjC{R1Xdx+FeU<z zN|_y$4;yRRz(yx2v-@dlS!ZcHel9x&arWu7XTuG<(Ig>ZLUI3<mGtdd+#w}AdbKvF zO1nX+%1!$qjL;?+Rl20<wUPfV@X?yz=Lb^9FX!&ivYS){!c4W$fSLY3$(U4gajiOB zw-shUXOhsC`pit1+xBkk@#OY<-77oM=2;o_P2CG)QnbB)@pS(fqB!XK$YJc#UUD%3 zVNVi1Msm;A<`ak!KA>dbUfH#GRvBWx?OO10ON++ya5l?QfPf2i*`z#Jh+F&~lo|WD z+n-jo!a!8fZjpSZXOnUwoX35bDQtgst^v;xk(|~d^DD-*q)4k!<xD+g&e$=76tRV> zl64k$>_D?^J8YY~P*Xao<aaJc-?Wp11?!AE&B{%=zyg(RNsM)t{kBuBOxCp#%Tn@5 z+V8$h-so2J>6IQ&KkR(#ria7Rw5Q+lCq>~CoumVfPw!t?X!f+L$E-gh9atqKcK<rz z023)qmOZIKgLh5ctJSh@*I;-|6N~rRopsLmT5g4V!0cLM#vAZvAh^uRqLDUWqC`>o zyZN8-HQU1jrf`@I7A^*3Y=8b}z*aR>7X~&{Z|r^TpR9UcS*N4qH{T&h4P`dipm!<S zhh6P9%g%@_$4Iv;L~9rA266(1Pi)GmQso<Y|C|VAAI%5uPqDVluzXRAAJ?{j;eFJ} zrR&-|t{GfU{L4~D$?&dn%PJswZH*}{EFe4;Vji>;JiSeKl)y|W{URi)(6a(U(T-?@ z<HSs`e!*tm{FV6}HO+sYf8zr;ztp1nR;r31&IYvGkIknl?Q<7q)1kUq=Y=N++XEaq z9gC`R86sZ>hThpp+4_Ljnk4b{J_}ouRepJ;tJ-s;CY0c64aX42bSd!__TN0qi9-TZ zX-e`6M`M^fcgOEWsRLirMKl!n=S`>n2{a#{ZpCvIG~AKlBpPXA8n`z)pHT>dyJPh5 zs)Q^N1@6Y_nn$&AQX7&sM&zpoE4*Q%&0#u_$rWQ<oZWUBxqpOXHTEBWNt@VS#*pMl zm}Au(zVUqH+l{tcjluaB%?EbB46=J8H3Ej}t~F2tCX8G?N*#0E4d9gb-V#N-%y57d zzir~oHy6|IP(Zw~mn&>7h0H46>m-k18*0|nywBKZ1BdDu8>?>}q@Iqx{L$d{V~ah5 zh-h4MN}oiSD~_KTmv_}IF3>iEY{LgX1)ydp6itc~H%&@cNLo%g!!X!NB<)Ky?PRpc zQUfq#4}q#EFrmJ4{WLZ<ki|6q8JN5*@!9V@bZp-6T2(#BXK2mA(vPCBA5)A}tLN2O z9SRR{ko_xeb1E!k>96qKB7Cj1YTw?)9Q;0U;?5&p3EWcG(D9tG(&oBZlW?c1rP}?L zQ{Hc6odB^}0Ma!OH+EG*DvXI+SGJzYoF5qkhNV*n5jfqtb+bJ@xsHk3-FGtkoNAL6 zt8;*jLL`hz=Y%9_T6e7}(^D!v)ksQ<X$5&sO%$AB>v3Iwgx^@j{dLF;8AG;VD|%!2 zLPN`JQwFRwF_l6;(Q=AudF()PWnEBIcJYCJLtL3tY0wS`-MKUNlJ8n!;{CjlTLc29 z>Oez($s!jz9Yufg96eBxX7pODH!G9L@9xO`kwt-&`^dEkn9~MY;jOQvY0TCw&a4&y z-NaZ+R8zKMWzIKiH);R^*NUtr72yXy%eBQnmP>1u$bD?xLb0$|{Su!K;P*uMgc062 zeA-)x*2om}G0eBG_+qdAd&NPQ5x;@9<jm_{K~EdlmdJv(GQ<RmIep<)P*ZqTXra>W z(AF4f%mzhac<~dv%69oHcK_MZO>VG><z&rSg7(k?Iqt(`8!*9nciPrIt0nD)R#$(G zxwrIX>~k;Xn`~*dxr6NTkRG3^15!Ml^31x$y48As0sZ~l<E@<TZpwSYW|QPdrJy@C zN%^6!cCMiPs>b-g&0coODmS01DM0U9II}HaSx3%uY^(p%?AiBzBUW>NFdF787cv*s zmzQO?aK&BkwFytp7wGK-UFz@S;psNOhknY=Ljmj7*w%}P-L210B5}{_%6R|u{@%$e z-CUDOBM9<Mc#dF)E}S(2`0`3Mef2h*7!Er1%On>X;>P{1FV*Qmq0jiNn+Qt=O#)er z^HsdXUE!Vi`+l5n4jC`KiKeS9H4)SavAO`htHbYTb<_Tg+TdcvRp#JOI%?}~*xw)9 zbt9a>Fe`)ZWvj{r8};Vk8mF0;a*b}<@?Jn%(#P{DZuy1>CmI;3!a^0seva40CW883 zt{^?wo;M$)+O<v>-8&?cgVUk$8i>RlG4N^{Z%MX?+H!TsgNV=GSN#LO>)mY&CTcWD z9mEU$6&6hE_{}XJ*?VZ2O4x>=`<>f2ikNnSe_xLOboc5F@7rnT3?A>V&DP)`D%WK5 zr5FAf`Mmj}aH8IOfpvX;0rP{)4&M1`LdRgLAZW)@tdVI)P+>QL&(zD~01lojS8e2M z&`^W`FV8PCUY+8(%G8B#m?uxG1^l_o{H~#vF+_mpX)V!)0Ta0nKfi1}xM$LCqE0ES zc5<Pn23qvA)a^Zq5R9GZGTk=ZMYcyxiBKS2cm}8vF}<(dH5B`V{l(vPRaNdC%Qk8i z;Q8Dv+Cu8Tub6)OP7Zns-11_rd>K><4;De0#R{*~(C!x3jb52~^Ee2*bjEm;!kC>h z05%lYMQ$%X7{F&}vUXbi_scUYq-!R7{C*UBoc44@E*;NXHdHbTUW&;h3?)#A`d|K1 zMJPg2{ts++Oy}Vl&WaF{E{u>_3=X+bv<WGi3b6VjiELRI=q+M&n%H6Ib9!NLY&r#8 zCS=KA0uHFkT8Ua7j`zxorM}<ejw8<O4>Bc;H5JFRv6@+k&ZFN(*9{8Bzlb&DoG;5x zEnYjJzOE6eI<GHYZ@wI-!@RoO$G0lw*JG)0E&*0X;Pl%x8cfa$TL5;K%k2fC%S9hG zVr*y|aMs++PtD_!CiYz>w{|HD9nL{*3BILjRt5^1F7GLeH3ed6CN@(3%3n1-|H)@K zmZ4flA}JQDzAwF_OjqN!47@J^g5LRmrdm`Li^^v1@JfZ9n(~iRDIM)fuErUoi018o zudxY`_`TFd>W-De)0B&k5Z(SSbUb=TC@vno8W|Yu^=NaAL_K~Je(#}#+dDk-c`d{( zdWP=!=}bJ~3Ga+h=NMc2apEt}_kC_pER_J-SJa2s^UhcF&Vie7$xqv<hTc&{i}|gD z7kuz50VN@6gBis4yPt89J#GJ?BmLq*J6k0OY2KT8n_wQD)v%gxB*m><+@S|6>8rJt z2A;l_!hes=c@6!GE-F@wO7md_Ri24m?72xlDfiU!zXS2E9sT+z?Jr>rRU~js;<)fw zT(hM}cin6#SXiU3M@BubDp=^*H_kC;slf_x>Y1;Suk^oFt$o2xq{y=-ZR>1oh}dGV z(OhN`S?>f=(q`I8c*<<_o9{vDgubCp@5QW~^A&W=nMagnpO_WZ`ILMn`S}8a3DHFD z4Vth<Z6<GSGBlU)p#ok7mkztTP_N{lR~`Cj*CzMbN8eRN-|AP?hr{>%7sOr~#5}q_ zC`KKt)NOcD;izo|<r*aU_92aCsBU}8C^E==ilBP3Hu90}v^O77F=v=5&dyPMQ1Ndq z`0kE#wwv^qv}oxGTdgC-v|tPRSJd%f7%uI_V}$Ngdn?Nyk9@_&YMC}=mwqWbWE&qQ zj=*V^Nh?}fMU}F~ao(&&Y()Efd(-8cW$40K+Nj6QdO^#>30Pd<-tF$6g&ci|dbv!e z0JP}B+OW)mseI+Lf;`^Z2fpV=ueP~@AS>?oG>Q{wNiM#gFKau%Wrbd#XiUy0ui6#H z!QrfoKUp<29>>dsC_<}Jvax0CXp1^JNGfC@z}@u^ov2mvWbXMmyVSST%Fg-LCU2TD zeQ=EZcpinj6ZYsMC5&ei_vKPlEoRwj*g3M=bm@8wiQ=nV*|w_g({*kSR~{)DyZYBX z_hO}2NzkQ+Y}6HHxs^WT8Xoz5)MvM_E!%jpShDb(lj~5~*MXQ!r6*-0j|!*H(|=ZV zJrr381UHKj4aXY-_R%v-^=V(iUBAx0b*^<AFV724TJksU<*759C16>jhJmv;XRw<Z z1SEtWOB&r@_xZY{gwLlJRGb1-?4BdWM*m~lHd5C=e62k7UG(RZoDuiH>@&9}tm6JA z7-Ra8hZlIaul74`<|d1p>LRP=bG@Z{8#c$$P89l&M1oOEZ3Cm4`6Z@OaEE|AwuoG( z&zS-V2;Tg`aUs)lb!XkoE2L>Bz&D<p->9Ld6}_+d@^X*id=|T7nxKNR-7Eu&<F+!e z@A*pRgSpH0d#!Li$iOst*DKEXtg!e+RFqHb*L0=wd{7}5+~ANSF)|<mQNlhH)jb~u zWV{|0ySA7x9_thPw_SJrXay&Gip^KdJnyY5d5$->-|lI|slTy;V&Kc^oeALzp|qMJ zSecmq{fHp>8n5Hf)%8^=JN8`TS&|6IJAzYCMc~eO?~3BZ!nyWB_Pd)l^b^#0rQwY~ za)mfr7$09*3BI(3eQ8j(nE8bqJHpL9$T?at7oM4GE>}ji!5Iqst#Il%cL8n;S^E7; zNh$AwIY0QxhtOv_LeS10j?PMQYm%#S1-Tr1NZkNxHa~4Rl)e#xZ@xU73z}Rzj*yfX z+lVmo%3V;%qrtXo$WL_H;hv++&g?TwRBx6IUbdJ+jtc1_d_;(vzFtoAOyZXy1dMzj zRt_D<5zDViZC#Q7p(|PV(z;GDt+S=w8*B#I<CNVe#dzO?KE!1|>pk<>E%u&m4jNB4 zVlq4K)=@sOAkm<8S0Jcv321g$>mq&BFvalo#ee9jup({CDlQoTMXk3q2NdmE^!d|E z8V1+bfy{K1Eb($Vp)9-gXs$eNb*F{>e&WfAB_1^E3I+PH**n+U0?oELJ|(1V`^Pf* zx4<vOU-#1v()9M%fgJm;>)VLKM8^qDO^dZsGo>rzi0Zf4#(Bm}skKT*yR2M>Dj!BB zq?DT1+#dV<!XLF-l8+w=r|K92f@0^_N^QT=YNVulV@+J6Rl<f6dmA=rH)?vAWg~mf z6_pfTxXrD8ukWp4ivuDV4y*H{hP~n@i<JDdh_tq=Ccp*dAZlWKBfI=wcZYnWRH4Rk z2b*yHe=o9we=(fCeoGQwKwUHP9=%MEepT~W;?t4qeM;X;n#iaB*iE~Jw;w!PG;Ues z-#yIFP$Ea#=aC;XkZe2&I;8L-nAzUxs-tP~9h84xXZ8fc>|WQY?&XSv)M~*W`8Pi9 z7D9QbU%i7UO9oM1x*LJzg%{XSLIl~)=8~9BjXLnc!3hR`;D?;2Db(|PA2?b`DLseI zuq{Bd7qVm$Ld$5IQkqV!mvpj93M&SB!)}E(?!Q)wdXILoW2OXl4{p`0JZ?{QpYqwu zn^~w>J-_{{RwRAI?qd54xV>87!AOn@w^vtF=h=;GlQGG&_1oEvs*sZ;&_t~_D{{H^ z_od@|gT2e|T<swcu6lU%VCv@dO<+R#ial$(co;bvU}%byf6;#$O4Ah=)*NbQFByN! z@)NbEX>GUaN|t7xBmFnJsAsnI`K%JX@YjuQ)xq7wl?k7*-)}t<W4cv_5Fm7Q*S?gg zl1xxZ@-DqoI^E4b*w*3~Xq<nlb9=zF<(t=&gbZ~X!J-dpzd`h*PK9sc64mS9hd#)! zwgWa@em<6_*_*{6Q^S15X%^!z8BN_)dY=<ePW2&P?0)zV-+oV;<=}*{<jP7SUj~f* z6PO;r6Ls3j)27j~@uJj^uyJ*uH7nwKwNyoe4zky|*hHJB$9s>@?2-NVnmK9iB<|<Z zWZ{AtWZ!TpNd<u9fmZwkN}0yx0lj}OR37_PVCgwH^upb#{+pQj<zeHb7&85O&@&U( zPuDWK#x({4b^X4;$qK=WA2AX7MQmH1W0r1a?+$y<n=zZe=bbrupFDd!i~)U4@qGK_ znY*k(8THZRl4$RERzsOJ=V(FA;F+gEKYK3Ar3Amk=*TN;t9JBLhtc}b-JoJAyDe#5 zS4~;fw{{iO(MeF(AHAC|2JRid-bu9EnGz6NknUl=p>$T}O0|ysG&_hf%Y!a|a5!5c zK2+EcMz=Z06^2y4l78g>wRdXIJw>B(IltO@r7|OoG@SLUm90KpcI%T?5nSSORq~ts zQ|3!9uR4zlUuuXdmKk~6dF}3v|N9q!s3!GE_al_aQ*VS@^>?(jOq=unqu|WrnST5@ zu2ec4Ng~HolFF6jwyDUG5Wcx>BuDO<+eW35Tv6_0<w&kAF`Hw^Fz1$Y?rRvv+~)S% z@4wIIkI!R|#~z>K{eB(K@8VhWgP9D~xtD)|x-F}-QD;(OG@mu5w3Uu3&Ih*$zq2l@ zMP-JK&#v{pN(pU^n-4BwX(4-}g=UF!FH3t^tG<Tq-g3BG<f3(V4x?1rQS=c;+<L%q z*CY+Cq1zTz*@ok<WjQ&C7WW;fr48rgG>Cs>M~+*UYh$Lg+KuV6iE^;1B;38<cH7hE z!y_Sm9Ez_n0XzPs%{`Vm+iD%|v}#gFjJe`2pAv<GHe>0LLV_fJP3>%}+)&z>f~cH3 zt9L`6$Z`bkYP>A(4A!9Bm#uSJ15Ow@bnKOzDJEI?cbE`m{p<yvCKryDR(E1MdVXy7 z&>=kHoMM+m7I5@+myFadmJy0z1AH)=yG2ZTrUb$zPQot9mXQd$fE1vZC21DAGs*bg z8F{E~=_bD=M4bQ7ku&mPnAOnCvU1<GEqV4ZgLe}UsSiVL{QVmOcUb+$dck}ZhbfFN zg>ZjAZQyhCWB#V$(aV--Ses^KS;DvY<r&XMVW^ykQvy#^{#!q9*>LY2T4+X~b7(CQ zRsjAb_NQ;OP|aW+g_m_SN7_Pqh|7Ml>hBc$1~%9X(*85JMK3^S2ub#x4u2y)ev1IR z{EPP~s5REWCyICAz{8>`WlNJYkJ=OEViVQzCc4X$;-ESg!tw~sV@*n6b}O@X4KMww zjcWVdn`Q>9D>l~fabCcwr@VKY-`U==&(IT+!3d=EDDhh+Iy;0AWxVwpOscn=dGg%s z)NI2Ng3GQU9?h9f>K%C1jBi93n!?cP*Y^~RK{ugZxdxT$C-S$CE;Ia88v92Bz*_?r zv>8EKl@UtSV;tR~8%eD>Ou=sxBRe2WzHEEq=5U4vNmL3eU$%X6&&EY+%Y%rm9QM!d zaHZUv5eTChfAiO;_iYk>fd+?>pa1=kp7jUE1*+2zZNK+!>yOT)xj8=@{oGG{w;#vl zHPrmx1kUx%hb8<6)r!Vm3KOq<;Lm Q%g_WN+BjUK0(et&OKnGmNrTe9<*Y)rI! zHQ}vK$<u)h)a&27?k+?{CEwv24f&l76$_ny4fQ6FIbDFvS%S;N5xLzsFNJpgm%!rV zAwVAX$QA#BaOL&PDF)sDx{@p%e{ZM9AU5dE>kfmheI@g(m)OCU;?|dOiNfKdw&p5b zXJmu#J<fU8@iRV`M<S2%9r;<5`uI*YSV*=I$F(tl^ULwk=FSA^Q34yuF(bZ^Ekd|t zn9zRk%Mx^9{HEm0aQdQ-{Lz4w1!h3?KmJ~kcttx1p~2tg&p@&9VG2^1QMVR7Yjtt4 z1BdU89C$R)`zNllt{TMk3W}G5-igG`NI9iXz@7t89^EY}(O1h><H_s|=!c3!2mIgh z4fNf~DLQm5ims-l`n&A}LurkJsxZt_(tLMF$ej-u?}IAMO_=DPzHRQ3T-V|<#&FGp z&JS0C%bB!hwQX`o^-pCH#jRqB@;zp*JNxDti^03q#BIOD8MqI=Wz+Lvt+CQo?+g3X znsK?%4Pzp<HsA=m7Sc=Qv-aZ%IWZDjT9dt3FRbV}9#C96P?myUC^(ZPqR~_QtfoIO zqs}BNfNk^qo%3qGob=n%oolOAVxnSq{yOElaMv|#VV)yi&-9hcNf7q(nXtot_vUet z(jP22oGU-TKND1EM%9~~v81(dMBihAuG*hJMeg~!9kkQ-!B18s1x(x$5n$E-zh4<^ zrP5ZfJO?SfCu^!dmve;QYBWAv`yDBiU9*s0==(O}JFoYUi1!ze;383lxny;Ov6xqv zK#&FkZ`?C%RcTXKY)`jv6eXrs|NfffK4$I^>%Wn@DMi>TGZj7Y%lKL7f@yiaK<yiO z2$ze?wHivxawINU_vbqQRXIK1R&(L~OKk9jzZdR!tbM@jv)zkU2mh8Rme|AnBlg*w z?X_PuNZCnuoQ|1H<(+7O^HDIv=yb~3SWiE4prFv*ec_qaxeg6fxlgHoUSxx=vV5u1 z2Ewl`PGh)o@^zMlTIHp>&j#mMp=KfWk+YVwh6cQ`p6?Vr+70rBtTuQhI_doIgQ^== zipI95ubr~$H4MUFeiwLmG<N+F8@WHZq_;lVt`ubK^<gAh)7k?nFKw-x5!xK|ldbCI z;P$@(s|S?!?d=%XBx%f_WTgx%hCyXgzr5d!ZT}Kk2@2G3^s+#xar|3%B|58Pb6heK z6Vs6L@y(R4_AvPF>pm6?p*~<}&VTIi_C|v}V(8uS7wP<pZ0#e(WKs8|V6e+nTCi=t znA25EowMC*(GDe0+vwaqBF)dYO|1UHo2uKPDuO-Y$qqx2SrWSqG!LJ)PXCE&Kf@`n z?c6>H#r}ol`Q(1*u@5h>m%%i9Wi;j~lEt-<HcM}rt;-JQ?!eKYb{ZHiT{ix(xvftw z*JN77N#$;n)%4O~wCLpSbGwPRrp6iHY=%#4ZeKIbNK3!!n*G(zN}gg{@y!O-m9{XU zs3OO-qqmw|883djT42lBS!PQ>tcI2`#5n|2zhpG4ldGs4TIZKOdcbhPh^ttFiPglZ zrN?dCL*gy1nP_jEuoUK;ebyC+KAU5m5qKQ0c#P)n>?H>nbTYBY2_Gi#eUC`ey78+j zdVV)VP@Z9Py5qp95-wyj_uR$qPuI$zz>+9>;=DYMIynBM3HBR!<e~eNnh`&w)=vjJ zAfpH$0OsvYhE&q?dvT|*uNbRc`s8yi+<d43>N#(9wylr){&tsFUgHN;><HhGxK4jD z)yWVp3XD~?qj4ect=EKSscj|D3+-f8lO>9)=_Q7E|AD^Df#{O0_MK{Xv9|m9@zxgk z27kt<&A?1`S)_7M<G%!;^uC70vhl7RTlW=<?`qwT^-UzQsKb)c+MThbvcK+dC_Y75 zLaWgBMRT^Z#s0sB>Cr^5rN3&tA&<Q&W;Xo;Hf8<MIr-b^pXF*JN?qKP3-8N#BTts* zx<y;g4%m1XO+eb3wr@a4W)uYZGgu<*RhJ1OBwxv7r-S#6F{AN{nDognY|SKmiPe_% z$*Ifw?>;@UYv7OIdAC#&@!=-#<@~|epP=NhB4gvPmF_SjhZ)(fK&$aJ)9ObMOJ%&V z{ViQ(=*bx(?|Dr{lS*R47zY<}Y1%@B^q+Urb!<rV#Oa2G#P`6eFBsnYm7U<Z4#b<0 zul9HliZ42>kKZx&$;hC=m}0U_{(!~`s6{a)8)>!jiLU*|DN2k5z`074tNQF6*SjiX zcV=^#JU^kWa1Z;XKB^jLXBRBQ<v)8zJzdobcmDh5r&n}2@)InTaE><|Qr-l;m8vOV z-iv>daufTq?|1PFNNDOlr4U^s7oz)L*VwP(96GAwkF<YaSsjx=d*eTnkDGqO8!c0q zyz!r(cy{n0OPgz3_%nWf$t2<R!NGOs9lg4#i!r#CuY#&q3Qjq4Cs+&9ZyQ%Mc1P1r z3p^@?)|``aEJZJR4P9JdS{D~vreW-gt4WLb<{32qfvhZBweDBEB}@l<IqCe9i=&V1 z60*!rUKaaq^d)}8l>Tta+C|5v@YQXdqoAQlKGZFgwRoR6+g;ud(%LtqKTBVzosm;r zNP|E4{Y3%<zJj8+Ul9QJMDskmKm6$htQ4JJ37$4Sj>fkDzJ-)MF2(%*T=^GOD$7?~ zoer51q76TdB>?zUJ+owOJ+3ZR47?VqY;nL%y7sPr<MCQ+x&Uu&%vCHGidER=8Hozr z)L`3wqR5iW9}mni8~{;bRcYQ=jmtU?*0&D88-+z!C>pxsE{%cyh?UyVp)jrmE5-GZ zpdcZ*-mYyRik|j$Io(BW|I*_h`s*~147<<FJF#4SnqI+=8qNLR5>wY%V5G5s1!4L& zkc>rCmGforiXKI|?uC>*6U1ZRp0E~U!T~57BoTF4y@Lq#kE{rmF!_Y`$3MAF<cmvd zXP+cA+9|=BoNPQ&^Y3V9-pG%<zu13S4KNm?zHfv|)crfvOHBhH=EsAicXHhiB)<3b zt~o6gx6DKcT{V_>5@^xWpA)DORtQ~P)9p|z@Q*j&BUB4r+a5HwVd}=%!-evE-@mG* zD)qkk<N4PN-Wx4kE0~<ytqpNCDUQ2#vgMQvjty4*XHq<Oq-QLElqm<B^O&09E_68o z@?jbh3@s2}K8+=8zK-qh_}n)KBRs{#g_XcMP24S72GfYb^#^|~X<pKFaG<&a_)Gis ztoqDeX#ASW%T<~namwq4BGQTDp+`KD{)s9mKjR~&c1c$Cchu>XA=WXx&G?X{dDRQQ z&1V{z->u0{MvS&`2ajcy^mgEFFcj7L;884iV{NLp47E_2z9!!Mq#&L}lbTdz44V77 zZS9?y^Uq5WJ9FYDE3kLrVXDw2QQ6A8^o_eLI?^hzW#dwO6Gu3!o4)qdFpz8I7=r}V zttT9x^7QnDyX0uO7i;iayS$DKRBkDezfUSVdT?4Dg_tGO%hw)H9Q98na4-NBq*<Mb zhEOHPho7V4{5+(pac=F@(i8!s8qUG_<&8*=K>wWCO+i0>#Wh<w`%q}6$^lGCepUK7 z8?hf55wEEPUoSnr9ag<iG-VS$iRL6C;b%HUle-g?B%PXV>BB?kS60s||Fjsh`vpuE zNs6@_ah_?22;@NqkH~RS5Q|tx!i45htiPu%wx4UjVrJe0wD+|}V?CB8a-g@~YEB<O zp@x=?yr#8EHM!f*wkUAQw6(@O&5fTo$CoUXMrIxgbNs;lmjDh_<q~nngf-YTn02+* zNHVhj#Z9IgAJC>q!XTEv7=PU9GkyWwWglSK)21=5_^mWFOH_7d!OAGE^pTv?R-+nj zL(#ix&ygS*zW@JzHU3uE+r=I03{j2f+Sd7AoXkJ8&u_!apWe)gQCynO_ZrxsqlY%9 zzk(Z*%b`(HrblV1mqvX59XAY@HYg6SoMTHfH9b*iI{#H$YHK<_x#TveZoBo%U5e@p zZ!HHsM_}sAEtWv^u9{g%|HwRShg?PNpv1($2f%PImARd-3q48(AzIc$<KnAX362<3 zUxdG6leK58Vs1{6r|n8*xR;pb>g4JN)}<_s;TW+yHB`M;%#HX(brE$^`strBRCoz& z=i&h4A$^0%*OO&z1v|B}xYmu*+#!<ivX%ZGv$y$H9hJ5~kE>p+q{57r+JygsRpSX_ z@~M%UmDKfKLyJ0E%EyelgCA3~-g_N{({k`EGpu33T&0Aqe8O_8Ma&=|y`>{HVdQIY z@D;b<!&zS4xqkk9O*paswwMI71#Oh%e<y#v4oBC6&R$cZ&jcPf6}oPBH9vzUb;q%N zS1$Be`g2N#p>y|~5$5N(9JPm)r{ECV{gNhSD|oOJsQzj1TtSWZ-cu2-gFc@Hsqy=@ zOR?clm4(fX);gdm;`QNBX{>MPnLjVD%mG*<n@Mk~3*<;Q%kSyxKOmM98A_=aLhX01 zBq{Ths#B}c!(bM;=1Kn_xsjLWauaNnV2ko3i8JTKGF0+>;tR~b3L`ll-Y{NL3u^<6 z6YeX+{@{~@R4W}*fI%K<6!?KA&)&1ZX68ZD>Vd^!)Kn_fX#J_N5k6L#CsZ`zj$&Y` zPxzL4B2@EeUW<T12GLjZc@Ao({)T!1wwGDeDv6YMZzJ-PEJ;XZsO21PIRusDiEDMj zyepqN+d#eCL1NDoy`ZuyDj+Zd)3Xq_z$n#SqWg-wi$5M0u~=1k^otDTnFuGYMjfxm zx1Xd`y^V%!w}hAnI#wrLbh}}3epn}QF*!GGY_UNp&ME1?5!FsD4y0h+c2yuZ1U`Z0 z3Y<8Y_5FqR&+B~c*Fwp)wE!vlco5J3z~d%V`z|JLU9o<@xas>whR%_hX}ryXO&Im0 z`=@d+6*)e9SuHF7CdSEdz7&?6(L9BW=0nl3`_n7gHmIwhO?r4JZqKn^72d%-+MTNH z04ppl+m1-k?yDkT#eaU)OFOH>F{J7uL!f!{o{7sLX5k6Y^CL#Fkn1?v9i#+5!q;nz z28_A1_viZJ&`_(petA__f*M0Jo#1eQasd+uOQoe!-xySOyy2GVrkkSQS33S{M|XI8 z#(t9N^fR7Szcl;zmP;k@O4caM_Fz?F>M7Pblp2B>_M{*}aLdI=p*pJZ@zUx$i{rL; zo(mtYdRNI~zfnK6nUr3(b$pTJHv3@4|AnlR!QX%FzS(<a{R{hUw&dg0@Vz0uT$nad z2Zz@!Iz#8B8`)G|2x^%>RrV)}nBqiw<Pn!^+{71YD5BJ}5yaig8qc`u-@E<Mm2MqO zblfm{fM4ZYS!>IfP-I$WmFe8>lNIz&|4-KKTdldx!L)&8RM`#!ikV!~6$5Z<_!-}U z-jplS(rn4<Ot?Ah_Uv+gWpdA8Wa-?{{cwE$Q|9n}DaU6We#K*yKOp%0<nLn~pA{<@ z802(l`K9#DK=;$k%5yT_KR(F0P%lDuwoe)-nvZKcRakngUjd`|uBi?tx7zK5L<y># zc2~_S$0uR`DwQ-&NjMIDlw1H&vLZk<4&BC-C5Nj>YEbr2>Yy1bYZ3Gl^-2srT?YL- zdmtq-$ennme({utE^yW|5dYu$^O8;{M!dZS2lA7XXh*H`so&C}S|7{P$TfPLe`p)< zG+HGcppa4?X*}WLx6*4EpEUIlp1-4)UiY~Zu%kDPjFAs+9!ZjK;-2h|WT@x#SUdOb z(|Gd-x4)02_rB)%5_)aq_`xfWkMXQ2&-3@Rhn&x<LroJ6YCj!JxHXPIQOO6txA2U8 zmSQs`MQ4uh^|wo)r70F9zgJCz+yfKVU8bQ~TQ52m$3p2C-3I|ObQa*FUs6mp;Z-xK zHe1v24-g>CX=BO-M<v6aJ`BCiT|bf*880-23=?_4Df1litED)|KxO^6g#P%wAqjyx zXDwkwteglWDrxNDOzK@9qRx@tuePSQ6w@T3(*jt<tc$h#bH$OYXq%|3L?MUP=n;n8 z+#eC3e)@^|=P<V~toy9;e)C$6YW*SI!fV&@P4e0p^t7w3bLoZ?@En4m=EyGXw(#bY z;uqW>?lb12iCqvMld}9PrV)R<{#)q~oY=v9xunak#T8oemQnXJ(v{~_f^*k8|9_R% zJY<jxu-5rCI~MLkbKCSGhOAxX(^Q6@my2TqMD4TJh0U(Gw43hF?Rf0~p{KsfT`P>; zP1f;5ew9z=ciQhkeJd=nmy*ZEHw7h`z{1Efl$KaKu?f?+Xar--$%=UWVN`~VGq^)_ zUQYr#rcPC<&P(r5#osSD$aXZ6Y;k`j8kan^$;-C+`tFt@Sq?F;$-TEoyK5-<y>4|J z5|s~g<=^A>t+=~nKGHQTza`JCPOe{UYtWre<F#nMKTs2nUS_z40EdH1md}j*>#)(E zZ?z5g!t#ivQD236#B+SSsY`B&yi6392Mz`8Hf4F8>~SF&9}H+-<!+Y9=^A#SX0O>$ zk4}s^*ZwoLxB?&XbTvuJG##)=O~=gaEe`xJ+VVTcOtw73`YgzKs+Cvo()jAPHVBA7 zIDV9~(xEO@GX=%GJ4psj?bt@9$kQ4hk#;pW)J)3&G?n0tn8<en=S0y^wd?Tpy{#uf zy)S>AAelf>nzE~$7MC&k-jls@Yuc`5`I<ijqR_h%uG!v?u(i~inD2<SF0{Mmmbz1J zSijhDF##%>ty68X2s@^Y9~uy76B`|b@k3%9LipTiE5WUq;Q*P%`Qz&L?qj9nRQeaE zPkmg4PIo;Z(h*V<8^_7AwM$&bh|xjV=tsu<c`|EOL2^S5`&MN3+5~tdAcOf<M^L;w zXh7n#tLRU;otEZDpjjR}98AQ~#<F1&YYF-1m~HM@fluz|337PyBc57z@!|EK0?aB# zMU0CuqkFQ+S>G$g=Y7`#NtYt<2z6ws@q9)YA}D(-uBn!}e_+uZH`#}?n*X6P73j3K zux(Nr)H{7Mq-@|lBc?J5>KV#&J8pFQq%dDdp?-X1LsR#N%fuEs(gt-n<1^$Q7NoL2 zosygZ2ZZh%;bxg%2^OHe<0PB<A=PJPX0b6<zyiKlv2%<|Vx}NJeb0u(xDTqsJ@~)u zFk#Vi1(F?(e-GpS0u~lNzJAi(hL1m*n~<9jWQD8Ft>Mbp3630q&xQY%bA>0I0uXWL zR>LEG`cJQ8?<^-2MDbg8%v91fg~P)IdBRl-;cN6L9KkFwIC&);t{|%*yyn$4>v!rR zaL_}yfw%dt1~gMHOU0*!CoWc#%7D!A8HcT3)a6tW@d-){VT??vSoz&!T-4ZkKDsP$ z6Hk{?B1Rj_F8`q(hgj327fVZ>g@&x@m2aQkn~-HLv;K$oX5^0=JA=6&DwN~qeI0k- ze$}e<OAi{-m9BNFBoZXCdHbRT&dEL@I2t*)+&H+^B!G?c<ie@X)qdGjlsbp#g2sYP zb4v4<niK`cLA{p(P9?maXIRZ2ztT*1Z@R3|*3&jc$_v$}aT{Z7OKQyC`;J!9dj(t% z!<bcdo!A%}boI-6Fn!A2>Jo+UcM`{9Pddx7KVH>lE$A@{lNrrI`vOk%*QJUBqb;V! zTU(+1YZ%c69K_QsMutyx!Qhc44;h*kxm<P?CZ?#M!x^@??E{wuq9SP;RdvKoxa#rt zNR3&43m(NC<F6y3Hl>DW7leT#Ycyf6B0PUmb?0jF8q=LqfE5j&d(&f#tsg7#@oZ%* zSeD-96vcL$OP-kQi_V(oTSCPe@JhD8P%^+RR=59Av88&u7c%;1-HbJLHgVgkYroD? z>OOYNp0q6A9uKJ|R`~pA8B#g(vPml34{|QB+QfZv$;hJbQnBLAmuaZdW<jff-apxk z%zIn0vmL&Tq{Hpe^)}RKVav<7SUPH2!@1q$e&vw`5a#55Sa|r$9N}c6Js`9?i25%O zl@0sZ8A*+L*gL2r;*rRA&f}(}#Qs=zZ_>*&xun}HG?jYg=ke$ag2sf1Zcsc&c#Q<+ zXMXt@zw`Q&M{YflSLZ4l`~K?zj{yd#q_z2yeLEr&=1g+lwgnK&jY!gXIiOhZ9K4+N ze>_hZjQnuuwnv1mjdx(~-0&IZ=AF$>?#YGn60B$5kx{LuK2pCzO5q-ZO-1{;oGr3E z{ypI=f5(LR2Fq@a=g|kMOeP+v5`%~0WzvM}d~HY9T#hP-g`zJlI@w_g%Sq{;J#%yQ z&mPe&=6=J!kh(&oddJRa003)Mx59klyQ*n#CZaiD=fYp4ISP<Ye9^v15m*XX_dM~H zSBBW>@L?4E>Craa-sj8zGlXiy;7Qg*xU?H;*(NIIA*@P{8@`NjD$m^-R=HEti}?n@ zqz2|uPIvUE&I7pxMcM!X6*!u<u4yQV6f{Sjcy>ur6>vfY_aB623K>pt$#lxS-TRN! zfrbrzv!+Y$bgqv_UA?W&Rtoau7qTc&RfqBaBlx;ZeG$;tfcxxBk|H8_G6b<e?y=zc z8DD(>vGuK=QNL(g!icAF&6{OXtu&<6TKQ;;aq<DSue7?ph|C~6w$^2HB|DB?iB$DD zat?{jaOTXwsiwI*5>gCN+1}i}5?yQb28nYNzkZ^xq5Nf0^OpkoKcy&IU6K|H&l}GR zKhv4_6S<R0FV2N1evp#ya`ey4tu2+=l>AK)GT4PsoruhqyEb3`du1Z!EloL)Fpe!k z%lzEz@DC=;t#)Vym|6h+9UZa*{@Aybe(p~H)k3OIuIdjO`6I(`#qqva%b{F$NA1Z< zLu=W^UOoFJX}#F0zHgUet9uG#NvEP55dcbIa!ymf_S$fRlI)(1X{E5Bj?tUk@y3?A z;-9`4jn64*?(vW_@1DOBm$NTzm|uY^5k~93w8nr%M~AST3|0VOJ?s@2)xV)Wr&dN! z%nzV9sO`%q+d7)aDHf1rENJB(A}~CSLu-3yU6rwC4ln)ay~CA0e)Gg!9wor;;Fb5G zNP}&+U1M)hK%qd5ZGiGE2W2<L`U6e=j+K7j&3X+zcq1asMFdHmX^aF)!lac<p;nS9 z)YZ@5#I5=IT`L_`!)_|&6PF!6Fyb=j@4QU8&S+G5k*H}XVdvwKbO)Sd97Z0B{t%hx z?G+lgM6UIq4XWBeVP%uDbX~UVRF}A9Yo%v}U9uPJzKBbF>y`v@L!CBymQLbi>tRmC z2^<*0+GsvX<wP=wPf~re=a+EHC&d(^vg=|>Ym*Wp#=BjGAs<A$ob(^ww7P$y%FWDK z4l}S;^!uns?fG7>TJwE5yr<<<ZpYvIoc^}sjZ>}OYDKplFMRPl$vOTBt1$@<)H+H= zzpr?6qXf+mhQ+q!3i#Ndps1$Tpk#rKK7gy(DdRj|new&k%>_;#Y4}dvI%!zU{#utl zZl|ogGYm`H%yjIk40M;t&z%f$w^lS$j=|762F;f`x8A`(J~!out}ra;y^`;-KU==2 zJZJq0ayA>c#(ORFspWV66@7;TK6SdHR&)0hi(l^Bum%TmYrvac)->rFy&@$nzX-}! zCx(gz*t%frs2|);$lPfehwud!iDT{$A+Ely@(Ukm&S;r5;ftGa%vuN|b8ssYS=%gi zLy*!I<;Q}t>6REUDUv5Ku{<2UR_mP!S-WvtJNA0hG><7#T5puDnw;a1vWxdef$$P! znl3);iwAdNQEjEpotgCW#kMk4mO<sJftn%7!DoS*sj$Cuui~XRL19>n?x+o07?k8+ zcMG<6_BqJ@K@lQ(vRupP{;>5Gye02y?99h1nXHf=tI8Sec3oKAy_c&sowhW&jVRsa zPDfj?K%C=veAd4$(!}^7#VYw6liy47Bldd1nL<sZyPxF#t5?VOk-n2;X!gX`s2DRL zB6Fv~iOLQef$`CMs%AcG%wD>&n;CC3KKYD!;!BLSkq+`JtW1@1cg?3Y>J&s#@&J5r z7liKZNRTf#%<my}*xjk#AS|-tKh@*navNfWa+I`=a()e1Swhv%5z-@FHS*TngwNIA z0+KPB<OF{eRSw(^iniks3h<jQi)yP}jd2p@#@!5GpZ!SQowj#5s$TNXK51Xlr_I>3 zBvxnbCiB$qO;vkfoJ43YW6PODM9(f?{F&tZ{qE32mEF8v5rH`t+mXr3VXk>Hk8nEk zrcYHT_A1%Yj)^N#C;P7{m0Qeue2_#5VQ8<MN>`G?N^)-q1ARpmUl54%YFnZ((c?cN zq(C6?%LVRB@VJ$u&V|`jstalaMQ&D?J(|vX^Z0;A$+GgVME@6{sMqz%6xN4MrD~OT zCG&+}d`H!<6U61r5l@=(;pz{qS>kOqlxHL$d^4fdia8!RO?+x`n9!NK@F`4ad6F+a z<#Oos_~vkAZX3?JBy3U(R^uQ_m+1gMO}2@T_m<_>EE}AT;2kMn`>PuQAo_AoPf>}t z27-N9Z<}4ZQ^PY-ld7<7A}L6gGWCwtL~7;0Myk8;Bdk!P0$7+AHcR&?$Ob;2ZPPs5 z9iS4{usOBA60@wiJ>{tdpLyFKHniia7I4i?W6g3YXtVj-uS1`Ml8~W$@wXPvm+`b@ z#xaQs6aa^kWOGvdoQ4A{HVOBmMez2Pujv<E?_Luqz`qfn5_Vcr29dMm$-TZyqV?AW z*j)VWo)S^JKhMWu;km0Sn}CTjRm3x)r=(e>wK(1p!m7;InfGSPueo!yE^L&$tc>r( zRvT+g$OlhkDRVQ&w=p|~bOe<1;z|<-0N{mZl&hP^H(_m=kyPF|w_+aVL@+x;^eYy; zpEfwLBuX1jF#uP>w*QcVIja&r!!(0Od6KJ|-V$J=2vtd?OR{UNnab3mc)K{ZOldo= zbMmm&2$u!ud?FB#x*t6bdc_J)CVNgRNN#9|)x|h`g>{2PB4L}bsbM`9kVI&urIHRg z>{RDCTr3@r>R0v@p@i)%M}(aTEg1f>lUCubc^G$l2&AgxxiwWBzJtoTYqdV^ztQhx zw0kEY)}kv#|Af#-*>eI}snWnRvOTa@(zG_)^>>88Q3T6plpJs(Mf0#iC!0sAY{7#A zwmBW>xk^9mg&+9IjxZ0Vy1D6@FZYard#4gz-r9epJ+csVDc2Z3aO5INFTWEKZq8jR zu0RG8>%niZy+M<Vj7T~7B!!ASAAhx%!F4#NvW~kIa8kV=wc9r`JwK-~OY1HC0fHYL z)`K*Hm@*GXWHmS9AfHtc-$fR_&D4q5l9Og(^>m*sYM9@FUh@y}wpkYS825nk!J+0` zoZ+amZyXY)m^~0GYG%lu)1dc@Z6vZJCld1KUpg3`1oG8>V}bkfitRy^mFfGpz=1Pg zwL<o#?ifne-Sh#^=gNJ1xtKq-$;W-f%fr?QGboTt{KFK59r$z=jDu`<$HOgpJZaIO z@cLuTgQe9;Ahx;YxXv`bI>hCu)%)nchsI^$>C%=+CkfYGt~%CN=R`ZoZd^nJt&ZD7 zGLQGjEmd#XN5B@aUsZkR<NL=>>D2rf^}F|vKd{N(L54iI`%_rSS16_SvqE~Unc>3e zLcxyjkLEJ_64A3a<uSI2*cms`FG$VxYX&8=A2=9@yPy-X^(!(HVGMFni?U5`)v|X> zSgD3F^rjCrMe~Xjql{3MQm4V4K_A*nqN+Eslnck5!|;7<Bs%<v-6^8=@-iFX|2lK5 z93n`C-q;aohV*~*QzQY=+m93+KkNv144SVS=|JD2-3+B#d|Rp7s-9wGphabIOD|?= zRSGV}mE&2U{cOlp(6-HS;BIQU=mxeQq|v`?EzUe-p((Q$*~gOCUVB%3@U)a#nj%ED zw*(O>_4hL2Evkg5f-F0uVNZ-E2cXskx5rnnT)U!qAVw&q98LXRIrd)h2=nYHx*PWE z?s@6YiPOdH@vHoNmnt6Iy5;7f_cH$Dp4gy-e3^r>BjIC!r>j=zVBTw8;fFc$@+}|j zALm>$dvn|5cHL8LPzkeLb<VmTL(C2=MUa|L0#>qL1c#DTTT&fS_!U)Jb||R|3#i|% zuWi|QvDP@Md`y!nFb>-!>~8Fuc^@V8x<i;}=53zf8<@<s+|G%nz$5PiW=eqZVR6Dy z;9|%awU@Q}_d1r<Q|i|uZyY^L$|~KuLJbEUclScVSDDkqOrAM*T2R-N2lIu`h}3G4 z=Ft|iN33I{Zfty72UPbYv_9ZD@W7@DHfaz3x_`CHZ`#gAaawf4HgiCg6Kw5#j&)?- zoD;T?@0|^ZJ~loS4&Nafc?XY#8lgi83I!ILN5j`e5jCoEH1~7I{OIj)Mz5GAVk;Wi zRWMPV>qIrm;I7&D7ZDE<SRIx`g790OnqeV(LxEY@L5p2b$uK{SSWyU*k?#ZXmaE(O zuoHK5u&{yLCE}>zs$ln^TKp<aVZXI3C2X9zQvc2SLLDd@p|KH1Gcv~S>KdbirwgCk zl;{63*{xrr5-4@+RlOIGy9K=|M^uQBTr-W!udC%QkewEC;i34mD%~|}*KfV*z12q6 z10@_Nor=kzKXN`#?u86~#pQxR_%Y6i*dg+K_Ac9Zi?dtim!Q4au~l=2*?hjAFZTa_ zU8?o<(U@{CskT}MkfZ|Rs-$AT3wec#79>QQw;NWWPze7Rt**E4ARFoV(kb}PvXmhC z#^$rf=rGMh@3)@&TFqfme@sZ#$Lsjs4klw(G_-~mc!e)%W=KeZn)V*xu{lN|6Y%|& zYT;Q0BnuvGM=145!<D~&s69Er_f7F9oJ!|%mPZG_mcV(6FqI7R8gOeBAc(2Dt|v<I znU(_>cLhTF`-PuD^PA&`sLe|~R5f2XQI7r5MG1S=In~-5D&4BIzZPmiFZ+X+gZssx zuKdLeaBY7+w#G|#e{uY#fF3?6gVXKdiFr9<a(~@dV!_I^^Y<}u<&|M!1%eF2l@Pcp zM`KUQ=BmiA`bnSfdh^!<`PYyn=h>?ViP~8iKlDA6OACcb##M~b-~PwrHli+xG68#E zQ1JM9wc2+$tN@L`002+h)d~#jIzg0N-X0AlzT{mV@K9Y2dcMw&ND0Ja>kcK?#f(z| zwd&Ymfk%|-KT<v&Xu@I8nLJ~|Fc3I8`1*L)KZP53&%G@01P`-xo3lX-c<^WxW+&q| zd-P=0cGTm?^`}~3@xw+;YBcJ{2=CZ41D*ug#ZSy>mrdp7;yKOZ;`-o_k<hT~$h{ji zEs?bI^SYPtTI<;GZH@xDgMCS(_-31Es?vNx;mAihukgmD00plUl-+DlHRW0KaSS`b zU@Jik%&`Y&dzf-$mzk>kIYw>mvJRj*)!Ull_xj8NCg@gbb==gGZ(cj!PgJPP1tOH5 zWCyJX@5c#|^QON;a)P(*G!Y>a`^Wdd`Qg!-X!>>h$q+-TDEaVZ?d`qnUdTb4_l$1A z)=5e1RWD&ys8lCsb;RBGk<b3{*4ZUP^;&{{;CS9b|I}0ljGJ3aENXo;zUxQQKA(`= z&i144<F|Q_Z(d{0<7iTb&3Kn&@;!~F{?i5X9Z;OGd|1shbmDOj`tWFrv?8!_oxJrB zjjpQOW>S?JJf9R8l02K`>!s&2DOv8HnJtS^kmfa&&x>0eycLaYtcQfx@aWN(TQ#A& zcp#I3JWPT|FEO$X$3TvFnLP$_e|DuE{XDz?^&oKRIR02Xq_3x4SFS@;>uY1!FUwLF zYvhR)1W8?1W|Y$yJ*X~X%_;=~>_a+cx`UqVWNGe#=iO@^Ehe+bc;)ecnEEJ3Y^ZPj zWrb(8tK({TL<pomE{qdCd6OL0B^81O5~oG3pieZpC%qmH`~3X}zM$3k3TN`gX9$gR zugm%!D_$>nk8X!KZM>YD?>t;$o-V-ktf(R`q5XM<Cm!M-?=Mnn8Jjc0NY5Pl8p-ah zBr<G!bH_cqb;98vx_Q3t`$%97zM|Hly2}&0zx??0mHT7K2j{9L<N}zveU*%5rM6|; zoiLsK2d{g}<101irk=>i1wSWP#6N#;w89ooKH<TBQL6nH3+CjW$B|jx0;}CjaV=Uo zjc~{iGhO2;>hwSjYK5NmBle5_hrNe*Rv?p8^kO{UAiGXz!RE<k>_nYOHGqpZu3BeD zXe6Tfi1N00?mB8~zDQGHYgz!be-Sym)fb0)19C1MdE<2<5Z1f_0P+uI$5nJp3W`Ae zf6TAlmhoS<nZj^nP3K>bNdUP$Ijj*_xuAJm#-jzHiq@PYhcAf-3r`08=#``?`TFUT zUa$J{5Z&rw<5~<2JndeHd!DxjyXDm)-ZFPLFh`JB74v&w%&6*m8pobc_ob;qeheTM zqO_}hken4ZG*`_S;QvQC;0tHcf`$L3p}oW>%Uk&~pp6xGX_n`!TQ-KZfOKGFw~aW0 zdg2Y&X#TH)u!+4rijmk}gEzA7pvrhG->kluR)pS)1g+W>cq6FpJe|GaV~^?u=-tf2 zPKEk?yx_t2&OX}lR-_VWmnS?p<RD7YSW~dRE?tZl@mwirj3^awdb417GiF6Rd{4bV zK=T;A>rO(a?lKFzi;w0H$<5KRYxMH9v40T%O#vMdnugR0=D|2MliGj5C}dW9VU?|b zB7SJU`RgM)Ol)>fVPx;W!CrDL@Hm~sXN<oSb_@>pE!KRn@z^9;t?7d9(QpF&-%YVd zQGvYL_q>=#2TA2UlIM3PuX96Ytvel!UCkulb{Qa)NxLUs?s85<ul%?(Vxj^>>3$8J z7oK-ekMz(Ux)BOp{_-TTYB_^Z#&g0v57NiG)$tH=yeL2wczd+FIeh_nZa(1v!Q;iQ zEgjW}!FAo%waHbvf+*^zTR&jj;pBqHN*RaPkvl(|)KDBRH>I8Jq<U_?po7|_j15bK zh{Y)sg)GNxI_b#uV88vs@s)5+su{v~`OH5rggWguF8>|6V!Gar+goGt84akJ9+~oT zOPxYfXze#cG<EqfB~!M4O&~NZAa<+TRBGI>iPMvmx4Xj>S5fgN><{h4Yl%r`;6MF6 zl6wtG(yxLDn-3k3?7x%(%Ln35L8YoPB*TGpb^p_w-!F#qe3t=46n;3LyIt$Q6u)^P zOXG;qv>LS1YT7vm=qk!~l>)V#;^lc?l?Me@>CsF_c@!X9FGM<lV+C{LE5Nwk2rQ*t zuKOwNsK{!QfGozxXMb+XWe6C?1_kk?R!oi)0Uv%mQI6is$-h;NyyrJrG?r2)E-KWV zMm~@1g339+`+?m3@tVx`xJ~Dpj#;4Sfu8NvL#MqR-sd6ECa+AT$;SCWJMR%sS-(3` z>;Ok|rn&v{b`sa$dj>fS;?bzotePLf7w>{CcxMQTy{c)tX*E}%H0Nwo>Kd9CsHnia zb9h?eqd>Xe(d%p9<(vcIfm~XDdW#{RhyXQhP)IcqM;a$*_s=M(gEUGcm;VW6A1?hM zzuHU{WrGMa2X=mlM*Xu?GRUz*_Sd*y@>I`q3vRx__?&tI1mCk=EL-td;&|ORD4zbx zK$9jvgRtMPXAi7@ZSP{%(~!npqBGp^0_|JTgcA0>bVF^L6Y0<s!Dj(TVuH!Vap}1A zz<2}2paB)Su0n<a!8Ts3yXpmKqhzpzB0tv;@~O~XWtH5kQ4_C4gs|rx?)@_*@L8zT z64NSQV$3xyV@dKHv6!gpUL^bEuq}1#{0#qZ^|m1?U*Tn|@|CP>5cYZUHD7_F1|8YQ zH1%H9g3J$Kf4uolpC0-~`GUWUnu^<A!x&*Bsct*B7~eOoXy6|^ZMmeYXsvSZiJ$8y zlnE()q4{mD*|!w)*#_a7EDl8z#1b*Ip_t@8yO~IkntpT&)>Q%tfndA)U_N4Gx&A^9 z85;C*+9RH=rOZh;zi^e^s>po{TmPf`6?}3`i>4ku%{MU?t;61iv)>#3M2bEC^~Bn^ zZtHtk?;RIF`LEs@v4B+OwML7C4=5NTp?olM6OXO~HfFbEYo&uaH>^Qrsf682ksNql zvtopbPL}iGp56t(TFX)XGQh_XIo<9nS1`EG?dW|{Mz=!)Cy!lGcIh68T@ZzY@46*r zLl1@3<A&rsOVrSB17aITw%VBNp_0DbTGajXK1&7U@rCoZJhNX+?(~wk#P%0qI=~q5 zGT==)qf~On7KhSiPZ2|Yxm45t?*v@<cXWWxkSB)%YwL2OgvdISmhU#Q<F&nOPG!8F zf!+x-qG_mh)86o^@+=BXxd7s-;zZ;YU-)Iw=Kp?bY>SHPra|*zqg#r)x|fZON`MN6 z{n^E9CDjZBHH=#QB2Kc~UusDyJ*{$uxhKRXup@-FnB)e(U0`nrx?Fx?I@K3#sAOni z?GxW#=%f?I9jUt_4DEzHum>cC%E$=r6p58?lmm#PBT$V|zOa63&~h#X(dLpF`d3{? zj+YCgyZ1hUPjmy|i{Sfe?mj@ZH1pKmQ_6Fi#9#+DOMQZ#FgJ%c7Pag``*UYif}70- zdHQ)sp?`-t-Zsrg3Yb|AR~(#uS^eWg#uNX@)sN;l4Sq+byyr6Lx-%ket^MksWtV_a zQUVi>Pp2g0@SVcht~K1*btWBBR}fLXy}zI$w#;P8;2Fzwei0Mb`!A52ccpI#|D2EZ zn}1-?!Df&VXcl9YNqdtf_no#i%o)B`)<ThtR&hlt1?AbvU3D*@Z%p>+e&C!8d1ibt zDdA-4llSMjyxU&lJg${7Hf3Info+-@D6e1pO3Wp8vyXS#(J9JnD*u$X^NuW68zb`@ zz}`r9LGGcz4K`!H(}Q<O6^;MYjl-HsL-3KAxJ9e3jXEpuSxHdhCR)pJ;d)Q53gIq8 zXkCa$BcXcc9-0dSUiSWJ^ID1_Tm-@vc3+~45T$6LD${Wqva|nq)9l<Qh%v>87kpdX zi(8D`hO;f=Pf~r^1LCaeB!7$JHO$8+6|3b!eV(bRluxDU0;5wlrz5V2&8d=(@(bam zb`b@-@UgD|6-td})y6pM>~+3s<P;&sT7gdv&cE=8Dk3Pf2dr*^hOC-#85_s0YSTWH zt!6v)yitG+L~>*@-}HZKfij_CiX)*(y%Axx7yg|UT`qn9rT0yD9cttUc6aBY4)$mO z8~mEvlJ1!)m5OQfB4u|?Ow_LBq?CK`nuofCXX@gOcD#V5Zsf<-qZ%_4tB3sK^!y%Q zGNMaS-)dfIR>?+0ZQ=sZ-#epYt_9$irBp$3<%DMpZH^qYm|}sGB+wWK5an9Pl2q$b zJ9hQ&#Bi@)!G>*5mu)e~k2HE$-GI+rlOR%FklTHffw?hf45BeNWq=uN9@Ogri5|Gn zt~dsFNS6h$opiONn6_#pw=1c4KwHClorO3mj*iXT%5~_1pRj_5J`em7qDN$?&9j-Z zH^{e1=`x;2qW0kATSB4p1de__XV(bvl!2XbwXlZ1FztAiQ!@IpJqIUDo-^G%)EUmF zzwIy8&0nIQrWIU*g#4qWf}-7jUP2nhTJQU2BYw(qk4HjCYV*0e)nnP^Wn#V$H&$Mg zOi7uY(Zt{LhV$i1g0wdQSh2TuM3L`(rz5!#xwjHN%=YwK-?|eOu@U4pC)kG>Hr~ED zgwh&-eqYfU6HfHr-4=So+Hb`->r5ayTL5z4EfW=5aDRUqN&;OL(hxzZCOxEehid4; z-v8xYFv)b#J$MEwgKf3fZ}%T|?HD{LulwA;Hwv&bHrfQQi=(pHfeN*I&)go33Pkp6 zE92A(-0Un>(Li)Im^int%SLf06h-xvt6bgen-<MQf>in$^uy`6y{3I%u~k@Dk9-7h zN()V$=PxEz!8GtauTAzluMYH$@E(5jpeYbkEL1eHSBZeUiCI>d09~CT4&XM9-|2d} z42uoLkrcCYq0g$2k?taBVBdWAfVG-Kokd6%EM^q%#@J}Zdn=KFsij@90w>1o&$5L! zox%-_D^oMkyDYLWG40su**lyXN?h{3;|q)0MHhJUuc-BFlGAi(a~I1@?kw;HqkZ9% zRb_AaSTpWlH@_}v-4zi*lS~>4*lq!og3rCc6jx8Lr#pZM!M@W)V%fNLKG5N{FUm9A z&>J_NbIn0NhVrPEExLeFg^2kNp>;)TYCY8-MKs;^+`k!JsC}_~R_DDLm(F`}*5EZc zKW}zQoP<jwZDj{~9;ryx`tDf3&DAU0hcY%4mOcs4jt9j-m;jkNDUaEyY}ZkCKuonx z&<zcYu1fs0WbvVry`(_;_JccmYg?Ge&Af!$S!WkN<;{O3=j^A0t>?ZQNc)6m{la1= z*+UpWb<fK?ZjR$ck45l{5lF_l`!Bbj7n>AQpCX)1pJc$)e_9nrMXb0m_juDkq1x)$ zJt`&Pu=wtbf#9=PKw##w!G>=Y=^Dx=vV|Vd=eDuYUke2#NJ0+gMNMub&x(uc^Fd{c zv}bEkSIgBU4N;Tj@xwPL-q^sk?g-6wkh9v?unKLBdBfgoF8gcypP0)is1Ol($}xg7 zy;=T&9@)kEH>|oH?M7^i>QwtIwR}EBK!i8kEyun1<8Urh)rfH*9rz3g{>qnybSO_7 zXJ6??#8uGzl90ha3ZXx@fZ%j$^td~DDoPnA+_3ErZQn>s*~rd)kvXMM;?{jpnS9{+ zFfv#J`pg%=*QOku{G*_Ly{NmPs^DG-OB~tjF7@&2Ok0>-gs)oh)Qz+M1lsjPkjOt? z22InfRdQfbVijRtn^BJ3YAQbPsi;Y9pLn0o)(rhv9SmJVpkQ`TI_2ZWO^a{ILq(Xo zACZC;sG*$!vPgXFQ0>BBa<K0WP?6mVpiXRI@|$~?#{8p^2Wu{W_fs?ZPalwo55oZ_ zOzgC;*=z&s=ahp&fa&U|yj<PKgcl5zZoGcz#!=@~-yrOCMHnbi8vo=^r|y>!wZ6+H zdGQOWgsOYGmvEi4Pr_lsx)p%y2%gLg=eOlzTW<22(cIG+mYUybr-tay%BeZ>hZkRJ zO9#18zwjD$Z&K-;-XSQfZDw|7*{64~0Jv@Ozvb--m%JB%nD}a~!gbL0?Hm%{N2JQ^ zrnBrLX^lyt;kZtI-8zX-%_(`8{hO1nuHdSA1_9iBPxA$)fO2*EA<~W9U(6CkM#5sX z3g!V}MuLR>qtwE%kj2hX-8YV!sO6nEeWR34VIz6k?Q{;6(5ryC451aMtZxz=5f}=X zOW4frQybqf@&=vwZYawgRQv(4hdIpzd3{~D{s=NO@d$VI)~+9F)wL7MS)F|5?~w30 zK<D6=l_uqmb)+V+@c`~ZOSWDx9i7|$&tWOjmMwcHss<`NCGu%~ao^lNUUV4gEwl~% z!G9%MHCHZC7|HJ1({)Yw1X(*KA$Yke)Lg_!@(wAOoZsQ}JwIu2T3CjF1mEDofFcP} zJqCWH-OrHrDFe6!Vzd{AJOR#iH>W)&Ck!ZN_kK;~%~Q<L{7eg(9Ls&F>+gH(_X8OV zMHQ2m<SOKwP_Cb?>)=`1#F@N}8WBY9#B4VON|9Pcz)mk7<_*w{W)jwXa*V2hY78&e z_oWYB#G`aHG<rHjLTsrbDrzcl=WfMhTJHn5oPDubAMLFg-DW9{6_a4QaaxrhW*e;1 z{DJ{+H5bIu8!jW{`8;EL-UW-{m=;--`63RGt_q$J2_U&^$aU9TLFa;Ktx-W`0`aIq zHQEht^oh&&@F&>^Cd5QEYfjy<HCDX%d_&>PH%(Ff!+{(6sJ{0nSW<9=9J(#aj_C2% z(VQ~FN$7v9u4!|QJrCl)IQ=H_)#iWPw&~qc2sFRtVzU_nY`}i&<$U5<nbh=`5)<<M z=Y%C=BybEf5f%Be+k-W$wjWQ#s%U2*RsJsYez;Q%GU2g1UyWlH`^LYj;oikL>sM~P zAKBu#4WFfqWVl{V_UqkDo%uNmp%{&_wk1Jm&{_K*bonU~OjZlL_A6f>M_Y{NIeG3~ zqfTGtFeKR1R_Ii-oOVymRFK{y5=Pr;+>Y9rEJbikpoxE7@zk{VI!K?Z3HAyd9jsJ; zU6~f-swp!Oq!v|*qz+W=R+cO%*Y$S=CQA-{8Uy?bxZBw_SO0O?QWOjO7uSGwZ^V3! z%RdYeE1#U|GXSHldMk}sF5PqHK)^fXp1YCUzKR75kWuG9Su;VIt0W#S0Viw^=`~wQ z%XJY&&4zUF$g=V6^blEJSCMQ}>}Wg{eNtTfDN2Ix-pfk?zi^ann+{oE0rV)*b`CYd zMgOpoU2`S#tgfB-L6zRbQ`*7Z0;W=Ji0#T2YpYdVgppteQ9IQw@<co>v35_SpApF& zbJoaqYE=>JD^>(7vVe&|W_1ri^x5qj-|M30lzT{?RXncgu^E&>s@2x&X9ZGOTIF7d zp^+F^`~SN5C)Y^}3{}DN<>C5^Y%n5r>hdA-ffTn$EQJu7_+R+VNw5@hS1sThk8#bp zACmV8^s7p>d)elSINgKG7-tbnvx?k%5X==_$ZSK#tvdBUbj(~0p#~XJF-mX`F@4be zwpS|_vMh#VPgVS9>!`Rk4BHdgfGd`}IfF0)hRtg?P=1rBjoQ5&NpsIdqC=RA+R~OV z*W+k^aH*l1p-Tv{HOgyx@UAYh0P2$~rq)|>4=G_-y0G!-qYAZw)sr(szU(*WJR044 z7#Ubvypq7s>!63kcb>5iiKR&S;7{_<_$R}T8^X9Jr)rld<<;P~9Q5-s9BcsIxXP~m zpdvQ5ljZu_kN%T1>^I@Vi^beFQdLyt9S1hlasXn6sh^9>oz*C~0$-Y2UC;f9Pb7D; zORZxMX|pGjx>{9w1<#D=$N%PtM?K-;vB2Yha+D0PxV_Kl2c8<{M^c)^Ar649qDZKC z58c1t6rICNq{*>&v+3bKztA<@1l_w~Tlyh_xBJrHsPPEr=#aG!Sbe2$OP6>k|8)K0 zGC#G5i}3#c7c~T-KQ)P6A<n6A=N(R)*FzmKfnvTn%wX`Y^@NUrTz6=jH{pi_AlNp? zL{n|rQ=@xe9^AgK@TS#E+Tniz=>Zo0^`AswG|)>#Lj<pVGroFyjSpCjGyj#wwfH)w zKF9HK4Llq@6`$2$iu}U|uGSTw%|lZoUi2K*RU<F_;JkJ(=jlUBbo~r-541B~#i)t+ z5WyA6;S_zr6UEU-bUnH2FTswyynm*v7_~<{6f559co#s47O_?HqibgQ46EL*;XP6? zV(no!&V$wH8qcdvH9XNV&z(QQ#c8edjcvzs8t2K0?xWms@*2Gd4y@MI^O72`S;U{{ zni&S~oaZxM7#gBum*&!G;X!QZcDxNf(>mt)HsNMA)6FnCSKNl@GXOmm@TUf&z3br6 zc|=QuqxR@r%eRfV&U5LsymwaL29wqoGr+uNu4CvF!ChB#*LzRR(dm<y_=2IG;paF! zX1&5HKEs$ntUb(tr=IC5R_ojJq9wYQa{h^T4!z?m9*y(v3r~bYPlQ*jYo!5K<R86o z)QU3<e8!!94S*)155u@(M|kB{zPwL)PCx6v6!U%8o?6#^9Q8W4>T`e3z4x~GYSy>w zIh*g8=RTwEW2UQEt?`@TGYoy54NeacoCr<?ud(u8drlu1Q8>BZ#vMP(&vBJk?Tgu9 w<6QuN2PM{xJ}CRT?&Ep|Ua2eaAl%0P54-glz<Ijw+5i9m07*qoM6N<$f+df)ivR!s literal 0 HcmV?d00001 From 04fbc9a22b6fc94e6a2956c530e7d91b94def8e4 Mon Sep 17 00:00:00 2001 From: ipcintron <ipcintron1@gmail.com> Date: Fri, 16 Feb 2024 02:15:27 -0600 Subject: [PATCH 0363/2145] change theme color --- client/nuxt.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/nuxt.config.js b/client/nuxt.config.js index 1736e3f6..b50ad787 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -95,7 +95,7 @@ module.exports = { meta: { appleStatusBarStyle: 'black', name: 'Audiobookshelf', - theme_color: '#373838', + theme_color: '#232323', mobileAppIOS: true, nativeUI: true }, @@ -103,7 +103,7 @@ module.exports = { name: 'Audiobookshelf', short_name: 'Audiobookshelf', display: 'standalone', - background_color: '#373838', + background_color: '#232323', icons: [ { src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg', From 24f1aae6b620417ee2a7311ade27d882a2f40627 Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Fri, 16 Feb 2024 11:44:25 -0300 Subject: [PATCH 0364/2145] Update pt-br.json Strings 541-766 --- client/strings/pt-br.json | 464 +++++++++++++++++++------------------- 1 file changed, 232 insertions(+), 232 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index ded3683e..806428e3 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -19,7 +19,7 @@ "ButtonClearFilter": "Limpar Filtro", "ButtonCloseFeed": "Fechar Feed", "ButtonCollections": "Coleções", - "ButtonConfigureScanner": "Configurar Scanner", + "ButtonConfigureScanner": "Configurar Verificador", "ButtonCreate": "Criar", "ButtonCreateBackup": "Criar Backup", "ButtonDelete": "Apagar", @@ -81,7 +81,7 @@ "ButtonShiftTimes": "Deslocar tempos", "ButtonShow": "Exibir", "ButtonStartM4BEncode": "Iniciar Codificação M4B", - "ButtonStartMetadataEmbed": "Iniciar Gravação de Metadados", + "ButtonStartMetadataEmbed": "Iniciar Inclusão de Metadados", "ButtonSubmit": "Enviar", "ButtonTest": "Testar", "ButtonUpload": "Upload", @@ -137,7 +137,7 @@ "HeaderMapDetails": "Designar Detalhes", "Headeritens": "Consultar", "HeaderMetadataOrderOfPrecedence": "Ordem de Prioridade dos Metadados", - "HeaderMetadataToEmbed": "Metadados a Serem Gravados", + "HeaderMetadataToEmbed": "Metadados a Serem Incluídos", "HeaderNewAccount": "Nova Conta", "HeaderNewLibrary": "Nova Biblioteca", "HeaderNotifications": "Notificações", @@ -165,7 +165,7 @@ "HeaderSettingsDisplay": "Exibição", "HeaderSettingsExperimental": "Funcionalidades experimentais", "HeaderSettingsGeneral": "Geral", - "HeaderSettingsScanner": "Scanner", + "HeaderSettingsScanner": "Verificador", "HeaderSleepTimer": "Timer", "HeaderStatsLargestItems": "Maiores Itens", "HeaderStatsLongestItems": "Itens mais longos (hrs)", @@ -284,7 +284,7 @@ "LabelFilename": "Nome do Arquivo", "LabelFilterByUser": "Filtrar por Usuário", "LabelFindEpisodes": "Localizar Episódios", - "LabelFinished": "Terminado", + "LabelFinished": "Concluído", "LabelFolder": "Pasta", "LabelFolders": "Pastas", "LabelFontBold": "Negrito", @@ -367,7 +367,7 @@ "LabelNextScheduledRun": "Próxima execução programada", "LabelNoEpisodesSelected": "Nenhum episódio selecionado", "LabelNotes": "Notas", - "LabelNotFinished": "Não terminado", + "LabelNotFinished": "Não concluído", "LabelNotificationAppriseURL": "URL(s) Apprise", "LabelNotificationAvailableVariables": "Variáveis disponíveis", "LabelNotificationBodyTemplate": "Modelo de Corpo", @@ -451,7 +451,7 @@ "LabelSettingsExperimentalFeatures": "Funcionalidade experimentais", "LabelSettingsExperimentalFeaturesHelp": "Funcionalidade em desenvolvimento que se beneficiairam dos seus comentários e da sua ajuda para testar. Clique para abrir a discussão no github.", "LabelSettingsFindCovers": "Localizar capas", - "LabelSettingsFindCoversHelp": "Se o seu audiobook não tiver uma capa incluída ou uma imagem de capa na pasta, o scanner tentará localizar uma capa.<br>Atenção: Isso irá estender o tempo de análise", + "LabelSettingsFindCoversHelp": "Se o seu audiobook não tiver uma capa incluída ou uma imagem de capa na pasta, o verificador tentará localizar uma capa.<br>Atenção: Isso irá estender o tempo de análise", "LabelSettingsHideSingleBookSeries": "Ocultar séries com um só livro", "LabelSettingsHideSingleBookSeriesHelp": "Séries com um só livro serão ocultadas na página de séries e na prateleira de séries na página principal.", "LabelSettingsHomePageBookshelfView": "Usar visão estante na página principal", @@ -538,230 +538,230 @@ "LabelUpdateDetails": "Atualizar Detalhes", "LabelUpdateDetailsHelp": "Permite sobrescrever detalhes existentes para os livros selecionados quando uma consulta for localizada", "LabelUploaderDragAndDrop": "Arraste e solte arquivos ou pastas", - "LabelUploaderDropFiles": "Drop files", - "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", - "LabelUseChapterTrack": "Use chapter track", - "LabelUseFullTrack": "Use full track", - "LabelUser": "User", - "LabelUsername": "Username", - "LabelValue": "Value", - "LabelVersion": "Version", - "LabelViewBookmarks": "View bookmarks", - "LabelViewChapters": "View Capítulos", - "LabelViewQueue": "View player queue", + "LabelUploaderDropFiles": "Solte os arquivos", + "LabelUploaderItemFetchMetadataHelp": "Busca título, autor e série automaticamente", + "LabelUseChapterTrack": "Usar a trilha do capítulo", + "LabelUseFullTrack": "Usar a trilha toda", + "LabelUser": "Usuário", + "LabelUsername": "Nome do usuário", + "LabelValue": "Valor", + "LabelVersion": "Versão", + "LabelViewBookmarks": "Ver marcadores", + "LabelViewChapters": "Ver capítulos", + "LabelViewQueue": "Ver fila do reprodutor", "LabelVolume": "Volume", - "LabelWeekdaysToRun": "Weekdays to run", - "LabelYourAudiobookDuration": "Your audiobook duration", - "LabelYourBookmarks": "Your Bookmarks", - "LabelYourPlaylists": "Your Lista de Reproduçãos", - "LabelYourProgress": "Your Progress", - "MessageAddToPlayerQueue": "Adicionar to player queue", - "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", - "MessageBackupsDescription": "Backups include users, user progress, Biblioteca item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your Biblioteca folders.", - "MessageBatchQuickMatchDescription": "Quick Match will attempt to Adicionar missing covers and metadata for the selected itens. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.", - "MessageBookshelfNoCollections": "You haven't made any collections yet", - "MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"", - "MessageBookshelfNoRSSFeeds": "No RSS feeds are open", - "MessageBookshelfNoSeries": "You have no series", - "MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook", - "MessageChapterErrorFirstNotZero": "First chapter must start at 0", - "MessageChapterErrorStartGteDuration": "Invalid start time must be less than audiobook duration", - "MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time", - "MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook", - "MessageCheckingCron": "Checking cron...", - "MessageConfirmCloseFeed": "Are you sure you want to close this feed?", - "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 Biblioteca \"{0}\"?", - "MessageConfirmDeleteLibraryItem": "This will delete the Biblioteca item from the database and your file system. Are you sure?", - "MessageConfirmDeleteLibraryItems": "This will delete {0} Biblioteca itens 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 Episódios as finished?", - "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all Episódios 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}\"?", - "MessageConfirmRemoveEpisode": "Are you sure you want to remove Episódio \"{0}\"?", - "MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} Episódios?", - "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", - "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", - "MessageConfirmRemovePlaylist": "Are you sure you want to remove your Lista de Reprodução \"{0}\"?", - "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all itens?", - "MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.", - "MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".", - "MessageConfirmRenameTag": "Are you sure you want to rename etiqueta \"{0}\" to \"{1}\" for all itens?", - "MessageConfirmRenameTagMergeNote": "Note: This etiqueta already exists so they will be merged.", - "MessageConfirmRenameTagWarning": "Warning! A similar etiqueta with a different casing already exists \"{0}\".", - "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} itens?", - "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", - "MessageDownloadingEpisode": "Downloading Episódio", - "MessageDragFilesIntoTrackOrder": "Drag files into correct track order", - "MessageEmbedFinished": "Embed Finished!", - "MessageEpisodesQueuedForDownload": "{0} Episódio(s) queued for download", - "MessageFeedURLWillBe": "Feed URL will be {0}", - "MessageFetching": "Fetching...", - "MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 etiquetas, OPF files, and text files will be scanned as new.", - "MessageImportantNotice": "Important Notice!", - "MessageInsertChapterBelow": "Insert chapter below", - "MessageItemsSelected": "{0} itens Selected", - "MessageItemsUpdated": "{0} itens Updated", - "MessageJoinUsOn": "Join us on", - "MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year", - "MessageLoading": "Loading...", - "MessageLoadingFolders": "Loading folders...", - "MessageM4BFailed": "M4B Failed!", - "MessageM4BFinished": "M4B Finished!", - "MessageMapChapterTitles": "Map chapter titles to your existing audiobook Capítulos without adjusting timestamps", - "MessageMarkAllEpisodesFinished": "Mark all Episódios finished", - "MessageMarkAllEpisodesNotFinished": "Mark all Episódios not finished", - "MessageMarkAsFinished": "Mark as Finished", - "MessageMarkAsNotFinished": "Mark as Not Finished", - "MessageMatchBooksDescription": "will attempt to match books in the Biblioteca with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.", - "MessageNoAudioTracks": "No audio tracks", - "MessageNoAuthors": "No Authors", - "MessageNoBackups": "No Backups", - "MessageNoBookmarks": "No Bookmarks", - "MessageNoChapters": "No Capítulos", - "MessageNoCollections": "No Collections", - "MessageNoCoversFound": "No Covers Found", - "MessageNoDescription": "No description", - "MessageNoDownloadsInProgress": "No downloads currently in progress", - "MessageNoDownloadsQueued": "No downloads queued", - "MessageNoEpisodeMatchesFound": "No Episódio matches found", - "MessageNoEpisodes": "No Episódios", - "MessageNoFoldersAvailable": "No Folders Available", - "MessageNoGenres": "No Genres", - "MessageNoIssues": "No Issues", - "MessageNoItems": "No itens", - "MessageNoItemsFound": "No itens found", - "MessageNoListeningSessions": "No Listening Sessions", - "MessageNoLogs": "No Logs", - "MessageNoMediaProgress": "No Media Progress", - "MessageNoNotifications": "No Notifications", - "MessageNoPodcastsFound": "No podcasts found", - "MessageNoResults": "No Results", - "MessageNoSearchResultsFor": "No search results for \"{0}\"", - "MessageNoSeries": "No Series", - "MessageNoTags": "No etiquetas", - "MessageNoTasksRunning": "No Tasks Running", - "MessageNotYetImplemented": "Not yet implemented", - "MessageNoUpdateNecessary": "No update necessary", - "MessageNoUpdatesWereNecessary": "No updates were necessary", - "MessageNoUserPlaylists": "You have no Lista de Reproduçãos", - "MessageOr": "or", - "MessagePauseChapter": "Pause chapter playback", - "MessagePlayChapter": "Listen to beginning of chapter", - "MessagePlaylistCreateFromCollection": "Create Lista de Reprodução from collection", - "MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching", - "MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer itensed metadata' server setting is enabled.", - "MessageRemoveChapter": "Remove chapter", - "MessageRemoveEpisodes": "Remove {0} Episódio(s)", - "MessageRemoveFromPlayerQueue": "Remove from player queue", - "MessageRemoveUserWarning": "Are you sure you want to permanently delete user \"{0}\"?", - "MessageReportBugsAndContribute": "Report bugs, request features, and contribute on", - "MessageResetChaptersConfirm": "Are you sure you want to reset Capítulos and undo the changes you made?", - "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", - "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your Biblioteca folders. If you have enabled server settings to store cover art and metadata in your Biblioteca folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.", - "MessageSearchResultsFor": "Search results for", - "MessageSelected": "{0} selected", - "MessageServerCouldNotBeReached": "Server could not be reached", - "MessageSetChaptersFromTracksDescription": "Set Capítulos using each audio file as a chapter and chapter title as the audio file name", - "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?", - "MessageThinking": "Thinking...", - "MessageUploaderItemFailed": "Failed to upload", - "MessageUploaderItemSuccess": "Successfully Uploaded!", - "MessageUploading": "Uploading...", - "MessageValidCronExpression": "Valid cron expression", - "MessageWatcherIsDisabledGlobally": "Watcher is disabled globally in server settings", - "MessageXLibraryIsEmpty": "{0} Biblioteca is empty!", - "MessageYourAudiobookDurationIsLonger": "Your audiobook duration is longer than the duration found", - "MessageYourAudiobookDurationIsShorter": "Your audiobook duration is shorter than duration found", - "NoteChangeRootPassword": "Root user is the only user that can have an empty password", - "NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.", - "NoteFolderPicker": "Note: folders already mapped will not be shown", - "NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS", - "NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your Episódios do not have a Pub Date. Some podcast apps require this.", - "NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate Biblioteca itens.", - "NoteUploaderOnlyAudioFiles": "If uploading only audio files then each audio file will be handled as a separate audiobook.", - "NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.", - "PlaceholderNewCollection": "New collection name", - "PlaceholderNewFolderPath": "New folder path", - "PlaceholderNewPlaylist": "New Lista de Reprodução name", - "PlaceholderSearch": "Search..", - "PlaceholderSearchEpisode": "Search Episódio..", - "ToastAccountUpdateFailed": "Failed to update account", - "ToastAccountUpdateSuccess": "Account updated", - "ToastAuthorImageRemoveFailed": "Failed to remove image", - "ToastAuthorImageRemoveSuccess": "Author image removed", - "ToastAuthorUpdateFailed": "Failed to update author", - "ToastAuthorUpdateMerged": "Author merged", - "ToastAuthorUpdateSuccess": "Author updated", - "ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)", - "ToastBackupCreateFailed": "Failed to create backup", - "ToastBackupCreateSuccess": "Backup created", - "ToastBackupDeleteFailed": "Failed to delete backup", - "ToastBackupDeleteSuccess": "Backup deleted", - "ToastBackupRestoreFailed": "Failed to restore backup", - "ToastBackupUploadFailed": "Failed to upload backup", - "ToastBackupUploadSuccess": "Backup uploaded", - "ToastBatchUpdateFailed": "Batch update failed", - "ToastBatchUpdateSuccess": "Batch update success", - "ToastBookmarkCreateFailed": "Failed to create bookmark", - "ToastBookmarkCreateSuccess": "Bookmark added", - "ToastBookmarkRemoveFailed": "Failed to remove bookmark", - "ToastBookmarkRemoveSuccess": "Bookmark removed", - "ToastBookmarkUpdateFailed": "Failed to update bookmark", - "ToastBookmarkUpdateSuccess": "Bookmark updated", - "ToastChaptersHaveErrors": "Capítulos have errors", - "ToastChaptersMustHaveTitles": "Capítulos must have titles", - "ToastCollectionItemsRemoveFailed": "Failed to remove item(s) from collection", - "ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection", - "ToastCollectionRemoveFailed": "Failed to remove collection", - "ToastCollectionRemoveSuccess": "Collection removed", - "ToastCollectionUpdateFailed": "Failed to update collection", - "ToastCollectionUpdateSuccess": "Collection updated", - "ToastItemCoverUpdateFailed": "Failed to update item cover", - "ToastItemCoverUpdateSuccess": "Item cover updated", - "ToastItemDetailsUpdateFailed": "Failed to update item details", - "ToastItemDetailsUpdateSuccess": "Item details updated", - "ToastItemDetailsUpdateUnneeded": "No updates needed for item details", - "ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished", - "ToastItemMarkedAsFinishedSuccess": "Item marked as Finished", - "ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished", - "ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished", - "ToastLibraryCreateFailed": "Failed to create biblioteca", - "ToastLibraryCreateSuccess": "Biblioteca \"{0}\" created", - "ToastLibraryDeleteFailed": "Failed to delete biblioteca", - "ToastLibraryDeleteSuccess": "Biblioteca deleted", - "ToastLibraryScanFailedToStart": "Failed to start scan", - "ToastLibraryScanStarted": "Biblioteca scan started", - "ToastLibraryUpdateFailed": "Failed to update biblioteca", - "ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" updated", - "ToastPlaylistCreateFailed": "Failed to create Lista de Reprodução", - "ToastPlaylistCreateSuccess": "Lista de Reprodução created", - "ToastPlaylistRemoveFailed": "Failed to remove Lista de Reprodução", - "ToastPlaylistRemoveSuccess": "Lista de Reprodução removed", - "ToastPlaylistUpdateFailed": "Failed to update Lista de Reprodução", - "ToastPlaylistUpdateSuccess": "Lista de Reprodução updated", - "ToastPodcastCreateFailed": "Failed to create podcast", - "ToastPodcastCreateSuccess": "Podcast created successfully", - "ToastRemoveItemFromCollectionFailed": "Failed to remove item from collection", - "ToastRemoveItemFromCollectionSuccess": "Item removed from collection", - "ToastRSSFeedCloseFailed": "Failed to close RSS feed", - "ToastRSSFeedCloseSuccess": "RSS feed closed", - "ToastSendEbookToDeviceFailed": "Failed to send ebook to dispositivo", - "ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"", - "ToastSeriesUpdateFailed": "Series update failed", - "ToastSeriesUpdateSuccess": "Series update success", - "ToastSessionDeleteFailed": "Failed to delete session", - "ToastSessionDeleteSuccess": "Session deleted", - "ToastSocketConnected": "Socket connected", - "ToastSocketDisconnected": "Socket disconnected", - "ToastSocketFailedToConnect": "Socket failed to connect", - "ToastUserDeleteFailed": "Failed to delete usuário", - "ToastUserDeleteSuccess": "User deleted" + "LabelWeekdaysToRun": "Dias da semana para executar", + "LabelYourAudiobookDuration": "Duração do seu audiobook", + "LabelYourBookmarks": "Seus Marcadores", + "LabelYourPlaylists": "Suas Listas de Reprodução", + "LabelYourProgress": "Seu Progresso", + "MessageAddToPlayerQueue": "Adicionar à lista do reprodutor", + "MessageAppriseDescription": "Para utilizar essa funcionalidade é preciso ter uma instância da <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API do Apprise</a> em execução ou uma api que possa tratar esses mesmos chamados. <br />A URL da API do Apprise deve conter o caminho completo da URL para enviar as notificações. Ex: se a sua instância da API estiver em <code>http://192.168.1.1:8337</code> você deve utilizar <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "Backups incluem usuários, progresso dos usuários, detalhes dos itens da biblioteca, configurações do servidor e imagens armazenadas em <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>não</strong> incluem quaisquer arquivos armazenados nas pastas da sua biblioteca.", + "MessageBatchQuickMatchDescription": "Consulta Rápida tentará adicionar capas e metadados ausentes para os itens selecionados. Ative as opções abaixo para permitir que a Consulta Rápida sobrescreva capas e/ou metadados existentes.", + "MessageBookshelfNoCollections": "Você ainda não criou coleções", + "MessageBookshelfNoResultsForFilter": "Sem Resultados para o filtro \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "Não existem feeds RSS abertos", + "MessageBookshelfNoSeries": "Você não tem séries", + "MessageChapterEndIsAfter": "O final do capítulo está além do final do seu audiobook", + "MessageChapterErrorFirstNotZero": "O primeiro capítulo precisa começar no 0", + "MessageChapterErrorStartGteDuration": "Tempo de início não é válido pois precisa ser menor do que a duração do audioboook", + "MessageChapterErrorStartLtPrev": "Tempo de início não é válido pois precisa ser igual ou maior que o tempo de início do capítulo anterior", + "MessageChapterStartIsAfter": "Início do capítulo está além do final do seu audiobook", + "MessageCheckingCron": "Verificando o cron...", + "MessageConfirmCloseFeed": "Tem certeza de que deseja fechar esse feed?", + "MessageConfirmDeleteBackup": "Tem certeza de que deseja apagar o backup {0}?", + "MessageConfirmDeleteFile": "Essa ação apagará o arquivo do seu sistema de arquivos. Tem certeza?", + "MessageConfirmDeleteLibrary": "Tem certeza de que deja apagar a biblioteca \"{0}\" definitivamente?", + "MessageConfirmDeleteLibraryItem": "Essa ação apagará o item da biblioteca do banco de dados e do seu sistema de arquivos. Tem certeza?", + "MessageConfirmDeleteLibraryItems": "Essa ação apagará {0} itens da biblioteca do banco de dados e do seu sistema de arquivos. Tem certeza?", + "MessageConfirmDeleteSession": "Tem certeza de que deseja apagar essa sessão?", + "MessageConfirmForceReScan": "Tem certeza de que deseja forçar a nova verificação?", + "MessageConfirmMarkAllEpisodesFinished": "Tem certeza de que deseja marcar todos os episódios como concluídos?", + "MessageConfirmMarkAllEpisodesNotFinished": "Tem certeza de que deseja marcar todos os episódios como não concluídos?", + "MessageConfirmMarkSeriesFinished": "Tem certeza de que deseja marcar todos os livros nesta série como concluídos?", + "MessageConfirmMarkSeriesNotFinished": "Tem certeza de que desejamarcar todos os livros nesta série como não concluídos?", + "MessageConfirmQuickEmbed": "Aviso! Inclusão rápida não fará backup dos seus arquivos de áudio. Verifique se tem um backup dos seus arquivos de áudio. <br><br>Quer continuar?", + "MessageConfirmRemoveAllChapters": "Tem certeza de que deseja remover todos capítulos?", + "MessageConfirmRemoveAuthor": "Tem certeza de que deseja remover o autor \"{0}\"?", + "MessageConfirmRemoveCollection": "Tem certeza de que deseja remover a coleção \"{0}\"?", + "MessageConfirmRemoveEpisode": "Tem certeza de que deseja remover o episódio \"{0}\"?", + "MessageConfirmRemoveEpisodes": "Tem certeza de que deseja remover os {0} episódios?", + "MessageConfirmRemoveListeningSessions": "Tem certeza de que deseja remover as {0} sessões de escuta?", + "MessageConfirmRemoveNarrator": "Tem certeza de que deseja remover o narrador \"{0}\"?", + "MessageConfirmRemovePlaylist": "Tem certeza de que deseja remover a sua lista de reprodução \"{0}\"?", + "MessageConfirmRenameGenre": "Tem certeza de que deseja renomear o gênero \"{0}\" para \"{1}\" em todos os itens?", + "MessageConfirmRenameGenreMergeNote": "Aviso: Este gênero já existe então eles serão combinados.", + "MessageConfirmRenameGenreWarning": "Atenção! Um gênero com um nome semelhante já existe \"{0}\".", + "MessageConfirmRenameTag": "Tem certeza de que deseja renomear a etiqueta \"{0}\" para \"{1}\" em todos os itens?", + "MessageConfirmRenameTagMergeNote": "Aviso: Esta etiqueta já existe então elas serão combinadas.", + "MessageConfirmRenameTagWarning": "Atenção! Uma etiqueta com um nome semelhante já existe \"{0}\".", + "MessageConfirmReScanLibraryItems": "Tem certeza de que deseja uma nova verificação de {0} itens?", + "MessageConfirmSendEbookToDevice": "Tem certeza de que deseja enviar {0} ebook(s) \"{1}\" para o dispositivo \"{2}\"?", + "MessageDownloadingEpisode": "Realizando o download do episódio", + "MessageDragFilesIntoTrackOrder": "Arraste os arquivo para ordenar as trilhas corretamente", + "MessageEmbedFinished": "Inclusão Concluída!", + "MessageEpisodesQueuedForDownload": "{0} Episódio(s) na fila de download", + "MessageFeedURLWillBe": "URL do Feed será {0}", + "MessageFetching": "Buscando...", + "MessageForceReScanDescription": "verificará todos os arquivos, como uma verificação nova. Etiquetas ID3 de arquivos de áudio, arquivos OPF e arquivos de texto serão tratados como novos.", + "MessageImportantNotice": "Aviso Importante!", + "MessageInsertChapterBelow": "Inserir capítulo abaixo", + "MessageItemsSelected": "{0} Itens Selecionados", + "MessageItemsUpdated": "{0} Itens Atualizados", + "MessageJoinUsOn": "Junte-se a nós", + "MessageListeningSessionsInTheLastYear": "{0} sessões de escuta no ano anteiror", + "MessageLoading": "Carregando...", + "MessageLoadingFolders": "Carregando pastas...", + "MessageM4BFailed": "Falha no M4B!", + "MessageM4BFinished": "M4B Concluído!", + "MessageMapChapterTitles": "Designar títulos de capítulos a partir dos capítulos existentes no audiobook sem ajustar seus tempos", + "MessageMarkAllEpisodesFinished": "Marcar todos episódios como concluídos", + "MessageMarkAllEpisodesNotFinished": "Marcar todos episódios como não concluídos", + "MessageMarkAsFinished": "Marcar como Concluído", + "MessageMarkAsNotFinished": "Marcar como Não Concluído", + "MessageMatchBooksDescription": "tentará consultar os livros da biblioteca no fornecedor de busca selecionado e preencher os detalhes ausentes e a capa. Não sobrescreve os detalhes.", + "MessageNoAudioTracks": "Sem trilhas de áudio", + "MessageNoAuthors": "Sem Autores", + "MessageNoBackups": "Sem Backups", + "MessageNoBookmarks": "Sem Marcadores", + "MessageNoChapters": "Sem Capítulos", + "MessageNoCollections": "Sem Coleções", + "MessageNoCoversFound": "Nenhuma Capa Encontrada", + "MessageNoDescription": "Sem Descrições", + "MessageNoDownloadsInProgress": "Não existem downloads em andamento", + "MessageNoDownloadsQueued": "Não existem itens na fila de download", + "MessageNoEpisodeMatchesFound": "Não existem episódios correspondentes", + "MessageNoEpisodes": "Sem Episódios", + "MessageNoFoldersAvailable": "Nenhuma Pasta Disponível", + "MessageNoGenres": "Sem Gêneros", + "MessageNoIssues": "Sem Problemas", + "MessageNoItems": "Sem Itens", + "MessageNoItemsFound": "Nenhum item encontrado", + "MessageNoListeningSessions": "Sem Sessões de Escuta", + "MessageNoLogs": "Sem Logs", + "MessageNoMediaProgress": "Sem Progresso de Mídia", + "MessageNoNotifications": "Sem Notificações", + "MessageNoPodcastsFound": "Nenhum podcasts encontrado", + "MessageNoResults": "Sem resultados", + "MessageNoSearchResultsFor": "Sem resultados para \"{0}\"", + "MessageNoSeries": "Sem Séries", + "MessageNoTags": "Sem etiquetas", + "MessageNoTasksRunning": "Sem Tarefas em Execução", + "MessageNotYetImplemented": "Ainda não implementado", + "MessageNoUpdateNecessary": "Não é necessária a atualização", + "MessageNoUpdatesWereNecessary": "Nenhuma atualizacão é necessária", + "MessageNoUserPlaylists": "Você não tem listas de reprodução", + "MessageOr": "ou", + "MessagePauseChapter": "Pausar reprodução do capítulo", + "MessagePlayChapter": "Escutar o início do capítulo", + "MessagePlaylistCreateFromCollection": "Criar uma lista de reprodução a partir da coleção", + "MessagePodcastHasNoRSSFeedForMatching": "Podcast não tem uma URL do feed RSS para ser usada na consulta", + "MessageQuickMatchDescription": "Preenche detalhes vazios do item & capa com o primeiro resultado de '{0}'. Não sobrescreve detalhes a não ser que a configuração 'Preferir metadados consultados' do servidor esteja ativa.", + "MessageRemoveChapter": "Remover capítulo", + "MessageRemoveEpisodes": "Remover {0} episódio(s)", + "MessageRemoveFromPlayerQueue": "Remover da lista do reprodutor", + "MessageRemoveUserWarning": "Tem certeza de que deseja apagar definitivamente o usuário \"{0}\"?", + "MessageReportBugsAndContribute": "Reporte bugs, peça funcionalides e contribua em", + "MessageResetChaptersConfirm": "Tem certeza de que deseja resetar os capítulos e desfazer as alterações realizadas?", + "MessageRestoreBackupConfirm": "Tem certeza de que deseja restaurar o backup criado em", + "MessageRestoreBackupWarning": "Restaurar um backup sobrescreverá totalmente o banco de dados localizado em /config e as imagens de capa em /metadata/items & /metadata/authors.<br /><br />Backups não alteram quaisquer arquivos nas pastas da sua biblioteca. Se a configuração do servidor de armazenar a arte da capa e os metadados nas pastas da sua biblioteca estiver ativa, esses itens não estão no backup e não serão sobrescritos.<br /><br />Todos os clientes usando o seu servidor serão atualizados automaticamente.", + "MessageSearchResultsFor": "Resultado da busca por", + "MessageSelected": "{0} selecionado(s)", + "MessageServerCouldNotBeReached": "Não foi possível estabelecer conexão com o servidor", + "MessageSetChaptersFromTracksDescription": "Definir os capítulos usando cada arquivo de áudio como um capítulo e o nome do arquivo como o título do capítulo", + "MessageStartPlaybackAtTime": "Iniciar a reprodução de \"{0}\" em {1}?", + "MessageThinking": "Pensando...", + "MessageUploaderItemFailed": "Falha no upload", + "MessageUploaderItemSuccess": "Uploaded realizado!", + "MessageUploading": "Realizando o upload...", + "MessageValidCronExpression": "Expressão do cron válida", + "MessageWatcherIsDisabledGlobally": "Monitoramento está desativado nas configurações do servidor", + "MessageXLibraryIsEmpty": "Biblioteca {0} está vazia!", + "MessageYourAudiobookDurationIsLonger": "A duração do seu audiobook é maior do que a duração encontrada", + "MessageYourAudiobookDurationIsShorter": "A duração do seu audiobook é menor do que a duração encontrada", + "NoteChangeRootPassword": "O usuário Admiistrador é o único usuário que pode não ter uma senha", + "NoteChapterEditorTimes": "Aviso: O tempo de início do primeiro capítulo precisa ficar em 0:00 e o tempo de início do último capítulo não pode exceder a duração deste audiobook.", + "NoteFolderPicker": "Aviso: pastas já designadas não serão exibidas", + "NoteRSSFeedPodcastAppsHttps": "Atenção: A maioria dos aplicativos de podcasts requer que a URL do feed RSS use HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "Atenção: Um ou mais dos seus episódios não tem uma data de publicação. Alguns aplicativos de podcasts requerem isto.", + "NoteUploaderFoldersWithMediaFiles": "Pastas com arquivos de mídia serão tratadas como itens de biblioteca distintos.", + "NoteUploaderOnlyAudioFiles": "Ao subir apenas arquivos de áudio, cada arquivo será tratado como um audiobook distinto.", + "NoteUploaderUnsupportedFiles": "Arquivos não suportados serão ignorados. Ao escolher ou arrastar um pasta, outros arquivos que não estão em uma pasta dentro do item serão ignorados.", + "PlaceholderNewCollection": "Novo nome da coleção", + "PlaceholderNewFolderPath": "Novo caminho para a pasta", + "PlaceholderNewPlaylist": "Novo nome da lista de reprodução", + "PlaceholderSearch": "Buscar..", + "PlaceholderSearchEpisode": "Buscar Episódio..", + "ToastAccountUpdateFailed": "Falha ao atualizar a conta", + "ToastAccountUpdateSuccess": "Conta atualizada", + "ToastAuthorImageRemoveFailed": "Falha ao remover imagem", + "ToastAuthorImageRemoveSuccess": "Imagem do autor removida", + "ToastAuthorUpdateFailed": "Falha ao atualizar o autor", + "ToastAuthorUpdateMerged": "Autor combinado", + "ToastAuthorUpdateSuccess": "Autor atualizado", + "ToastAuthorUpdateSuccessNoImageFound": "Autor atualizado (nenhuma imagem encontrada)", + "ToastBackupCreateFailed": "Falha ao criar backup", + "ToastBackupCreateSuccess": "Backup criado", + "ToastBackupDeleteFailed": "Falha ao apagar backup", + "ToastBackupDeleteSuccess": "Backup apagado", + "ToastBackupRestoreFailed": "Falha ao restaurar backup", + "ToastBackupUploadFailed": "Falha no upload do backup", + "ToastBackupUploadSuccess": "Uploado do backup realizado", + "ToastBatchUpdateFailed": "Falha na atualização em lote", + "ToastBatchUpdateSuccess": "Atualização em lote realizada", + "ToastBookmarkCreateFailed": "Falha ao criar marcador", + "ToastBookmarkCreateSuccess": "Marcador adicionado", + "ToastBookmarkRemoveFailed": "Falha ao remover marcador", + "ToastBookmarkRemoveSuccess": "Marcador removido", + "ToastBookmarkUpdateFailed": "Falha ao atualizar o marcador", + "ToastBookmarkUpdateSuccess": "Marcador atualizado", + "ToastChaptersHaveErrors": "Capítulos com erro", + "ToastChaptersMustHaveTitles": "Capítulos precisam ter títulos", + "ToastCollectionItemsRemoveFailed": "Falha ao remover item(ns) da coleção", + "ToastCollectionItemsRemoveSuccess": "Item(ns) removidos da coleçào", + "ToastCollectionRemoveFailed": "Falha ao remover coleção", + "ToastCollectionRemoveSuccess": "Coleção removida", + "ToastCollectionUpdateFailed": "Falha ao atualizar coleção", + "ToastCollectionUpdateSuccess": "Coleção atualizada", + "ToastItemCoverUpdateFailed": "Falha ao atualizar capa do item", + "ToastItemCoverUpdateSuccess": "Capa do item atualizada", + "ToastItemDetailsUpdateFailed": "Falha ao atualizar detalhes do item", + "ToastItemDetailsUpdateSuccess": "Detalhes do item atualizados", + "ToastItemDetailsUpdateUnneeded": "Nenhuma atualização necessária para os detalhes do item", + "ToastItemMarkedAsFinishedFailed": "Falha ao marcar como Concluído", + "ToastItemMarkedAsFinishedSuccess": "Item marcado como Concluído", + "ToastItemMarkedAsNotFinishedFailed": "Falha ao marcar como Não Concluído", + "ToastItemMarkedAsNotFinishedSuccess": "Item marcado como Não Concluído", + "ToastLibraryCreateFailed": "Falha ao criar biblioteca", + "ToastLibraryCreateSuccess": "Biblioteca \"{0}\" criada", + "ToastLibraryDeleteFailed": "Falha ao apagar biblioteca", + "ToastLibraryDeleteSuccess": "Biblioteca apagada", + "ToastLibraryScanFailedToStart": "Falha ao iniciar verificação", + "ToastLibraryScanStarted": "Verificação da biblioteca iniciada", + "ToastLibraryUpdateFailed": "Falha ao atualizar a biblioteca", + "ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" atualizada", + "ToastPlaylistCreateFailed": "Falha ao criar lista de reprodução", + "ToastPlaylistCreateSuccess": "Lista de reprodução criada", + "ToastPlaylistRemoveFailed": "Falha ao remover lista de reprodução", + "ToastPlaylistRemoveSuccess": "Lista de reprodução removida", + "ToastPlaylistUpdateFailed": "Falha ao atualizar lista de reprodução", + "ToastPlaylistUpdateSuccess": "Lista de reprodução atualizada", + "ToastPodcastCreateFailed": "Falha ao criar podcast", + "ToastPodcastCreateSuccess": "Podcast criado", + "ToastRemoveItemFromCollectionFailed": "Falha ao remover item da coleção", + "ToastRemoveItemFromCollectionSuccess": "Item removido da coleção", + "ToastRSSFeedCloseFailed": "Falha ao fechar feed RSS", + "ToastRSSFeedCloseSuccess": "Feed RSS fechado", + "ToastSendEbookToDeviceFailed": "Falha ao enviar ebook para dispositivo", + "ToastSendEbookToDeviceSuccess": "Ebook enviado para o dispositivo \"{0}\"", + "ToastSeriesUpdateFailed": "Falha ao atualizar série", + "ToastSeriesUpdateSuccess": "Série atualizada", + "ToastSessionDeleteFailed": "Falha ao apagar sessão", + "ToastSessionDeleteSuccess": "Sessão apagada", + "ToastSocketConnected": "Socket conectado", + "ToastSocketDisconnected": "Socket desconectado", + "ToastSocketFailedToConnect": "Falha na conexão do socket", + "ToastUserDeleteFailed": "Falha ao apagar usuário", + "ToastUserDeleteSuccess": "Usuário apagado" } From e9591caf812eb7b9e799e18dce43dafe3053edd2 Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Fri, 16 Feb 2024 11:56:31 -0300 Subject: [PATCH 0365/2145] Spelling --- client/strings/pt-br.json | 52 +++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 806428e3..148d53d5 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -40,7 +40,7 @@ "ButtonLookup": "Procurar", "ButtonManageTracks": "Gerenciar Faixas", "ButtonMapChapterTitles": "Designar Títulos de Capítulos", - "ButtonitensAllAuthors": "Consultar Todos Autores", + "ButtonitensAllAuthors": "Consultar Todos os Autores", "ButtonitensBooks": "Consultar Livros", "ButtonNevermind": "Cancelar", "ButtonNextChapter": "Próximo Capítulo", @@ -148,7 +148,7 @@ "HeaderPermissions": "Permissões", "HeaderPlayerQueue": "Fila do reprodutor", "HeaderPlaylist": "Lista de Reprodução", - "HeaderPlaylistItems": "Intens da lista de reprodução", + "HeaderPlaylistItems": "Itens da lista de reprodução", "HeaderPodcastsToAdd": "Podcasts para Adicionar", "HeaderPreviewCover": "Visualização da Capa", "HeaderRemoveEpisode": "Remover Episódio", @@ -190,9 +190,9 @@ "LabelAdded": "Acrescentado", "LabelAddedAt": "Acrescentado em", "LabelAddToCollection": "Adicionar à Coleção", - "LabelAddToCollectionBatch": "Adicionar {0} Livros à Coleçào", + "LabelAddToCollectionBatch": "Adicionar {0} Livros à Coleção", "LabelAddToPlaylist": "Adicionar à Lista de Reprodução", - "LabelAddToPlaylistBatch": "Adicionar {0} itens to Lista de Reprodução", + "LabelAddToPlaylistBatch": "Adicionar {0} itens à Lista de Reprodução", "LabelAdminUsersOnly": "Apenas usuários administradores", "LabelAll": "Todos", "LabelAllUsers": "Todos Usuários", @@ -208,7 +208,7 @@ "LabelAutoFetchMetadata": "Buscar Metadados Automaticamente", "LabelAutoFetchMetadataHelp": "Busca metadados de título, autor e série para otimizar o upload. Pode ser necessário buscas metadados adicionais após o upload.", "LabelAutoLaunch": "Iniciar Automaticamente", - "LabelAutoLaunchDescription": "Redireciona para o fornecedor de autenticação automaticamente ao navegar para a tela de login (caminhp para substituição manual <code>/login?autoLaunch=0</code>)", + "LabelAutoLaunchDescription": "Redireciona para o fornecedor de autenticação automaticamente ao navegar para a tela de login (caminho para substituição manual <code>/login?autoLaunch=0</code>)", "LabelAutoRegister": "Registrar Automaticamente", "LabelAutoRegisterDescription": "Registra automaticamente novos usuários após login", "LabelBackToUser": "Voltar para Usuário", @@ -319,7 +319,7 @@ "LabelInvert": "Inverter", "LabelItem": "Item", "LabelLanguage": "Idioma", - "LabelLanguageDefaultServer": "Idoma Padrão do Servidor", + "LabelLanguageDefaultServer": "Idioma Padrão do Servidor", "LabelLastBookAdded": "Último Livro Acrescentado", "LabelLastBookUpdated": "Último Livro Atualizado", "LabelLastSeen": "Visto pela Última Vez", @@ -335,7 +335,7 @@ "LabelLibraryName": "Nome da Biblioteca", "LabelLimit": "Limite", "LabelLineSpacing": "Espaçamento entre linhas", - "LabelListenAgain": "Ouvir novamente", + "LabelListenAgain": "Escutar novamente", "LabelLogLevelDebug": "Debug", "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Atenção", @@ -344,7 +344,7 @@ "LabelMatchExistingUsersBy": "Consultar usuários existentes usando", "LabelMatchExistingUsersByDescription": "Utilizado para conectar usuários já existentes. Uma vez conectados, usuários serão consultados utilizando uma identificação única do seu provedor de SSO", "LabelMediaPlayer": "Reprodutor de mídia", - "LabelMediaType": "Típo de Mídia", + "LabelMediaType": "Tipo de Mídia", "LabelMetadataOrderOfPrecedenceDescription": "Fontes de metadados de alta prioridade terão preferência sobre as fontes de metadados de prioridade baixa", "LabelMetadataProvider": "Fonte de Metadados", "LabelMetaTag": "Etiqueta Meta", @@ -353,7 +353,7 @@ "LabelMissing": "Ausente", "LabelMissingParts": "Partes Ausentes", "LabelMobileRedirectURIs": "URIs de redirecionamento móveis permitidas", - "LabelMobileRedirectURIsDescription": "Essa é uma lista de permissionamento para URIs válidas para o redirecionamento de aplicativos móveis. A padrão é <code>audiobookshelf://oauth</code>, que pode ser removida ou acrescentada com novas URIs para integração com apps de tereceiros. Usando um asterisco (<code>*</code>) como um item único dará permissão para qualquer URI.", + "LabelMobileRedirectURIsDescription": "Essa é uma lista de permissionamento para URIs válidas para o redirecionamento de aplicativos móveis. A padrão é <code>audiobookshelf://oauth</code>, que pode ser removida ou acrescentada com novas URIs para integração com apps de terceiros. Usando um asterisco (<code>*</code>) como um item único dará permissão para qualquer URI.", "LabelMore": "Mais", "LabelMoreInfo": "Mais Informações", "LabelName": "Nome", @@ -399,7 +399,7 @@ "LabelPodcastType": "Tipo de Podcast", "LabelPort": "Porta", "LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)", - "LabelPreventIndexing": "Evitar que o seu feed seja indexado pelos diretórios de podcast do iTunes and Google", + "LabelPreventIndexing": "Evitar que o seu feed seja indexado pelos diretórios de podcast do iTunes e Google", "LabelPrimaryEbook": "Ebook principal", "LabelProgress": "Progresso", "LabelProvider": "Fonte", @@ -443,7 +443,7 @@ "LabelSettingsChromecastSupport": "Suporte ao Chromecast", "LabelSettingsDateFormat": "Formato de data", "LabelSettingsDisableWatcher": "Desativar Monitoramento", - "LabelSettingsDisableWatcherForLibrary": "Desativa o monitoramento de pastas para a biblioteca", + "LabelSettingsDisableWatcherForLibrary": "Desativa o monitoramento de pastas para a biblioteca", "LabelSettingsDisableWatcherHelp": "Desativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor", "LabelSettingsEnableWatcher": "Ativar Monitoramento", "LabelSettingsEnableWatcherForLibrary": "Ativa o monitoramento de pastas para a biblioteca", @@ -515,14 +515,14 @@ "LabelTimeToShift": "Deslocamento de tempo em segundos", "LabelTitle": "Título", "LabelToolsEmbedMetadata": "Incluir Metadados", - "LabelToolsEmbedMetadataDescription": "Incuir metadados no arquivo de áudio, com imagem da capa e capítulos.", + "LabelToolsEmbedMetadataDescription": "Incluir metadados no arquivo de áudio, com imagem da capa e capítulos.", "LabelToolsMakeM4b": "Gerar audiobook no formato M4B", "LabelToolsMakeM4bDescription": "Gerar um arquivo de audiobook no formato .M4B com metadados, imagem da capa e capítulos.", "LabelToolsSplitM4b": "Dividir um M4B em MP3s", "LabelToolsSplitM4bDescription": "Criar arquivos MP3s a partir da divisão de um M4B em capítulos, com metadados e imagem de capa.", "LabelTotalDuration": "Duração Total", "LabelTotalTimeListened": "Tempo Total Escutado", - "LabelTrackFromFilename": "Trilha a partir do nome do arquvico", + "LabelTrackFromFilename": "Trilha a partir do nome do arquivo", "LabelTrackFromMetadata": "Trilha a partir dos Metadados", "LabelTracks": "Trilhas", "LabelTracksMultiTrack": "Várias trilhas", @@ -572,7 +572,7 @@ "MessageConfirmCloseFeed": "Tem certeza de que deseja fechar esse feed?", "MessageConfirmDeleteBackup": "Tem certeza de que deseja apagar o backup {0}?", "MessageConfirmDeleteFile": "Essa ação apagará o arquivo do seu sistema de arquivos. Tem certeza?", - "MessageConfirmDeleteLibrary": "Tem certeza de que deja apagar a biblioteca \"{0}\" definitivamente?", + "MessageConfirmDeleteLibrary": "Tem certeza de que deseja apagar a biblioteca \"{0}\" definitivamente?", "MessageConfirmDeleteLibraryItem": "Essa ação apagará o item da biblioteca do banco de dados e do seu sistema de arquivos. Tem certeza?", "MessageConfirmDeleteLibraryItems": "Essa ação apagará {0} itens da biblioteca do banco de dados e do seu sistema de arquivos. Tem certeza?", "MessageConfirmDeleteSession": "Tem certeza de que deseja apagar essa sessão?", @@ -580,9 +580,9 @@ "MessageConfirmMarkAllEpisodesFinished": "Tem certeza de que deseja marcar todos os episódios como concluídos?", "MessageConfirmMarkAllEpisodesNotFinished": "Tem certeza de que deseja marcar todos os episódios como não concluídos?", "MessageConfirmMarkSeriesFinished": "Tem certeza de que deseja marcar todos os livros nesta série como concluídos?", - "MessageConfirmMarkSeriesNotFinished": "Tem certeza de que desejamarcar todos os livros nesta série como não concluídos?", + "MessageConfirmMarkSeriesNotFinished": "Tem certeza de que deseja marcar todos os livros nesta série como não concluídos?", "MessageConfirmQuickEmbed": "Aviso! Inclusão rápida não fará backup dos seus arquivos de áudio. Verifique se tem um backup dos seus arquivos de áudio. <br><br>Quer continuar?", - "MessageConfirmRemoveAllChapters": "Tem certeza de que deseja remover todos capítulos?", + "MessageConfirmRemoveAllChapters": "Tem certeza de que deseja remover todos os capítulos?", "MessageConfirmRemoveAuthor": "Tem certeza de que deseja remover o autor \"{0}\"?", "MessageConfirmRemoveCollection": "Tem certeza de que deseja remover a coleção \"{0}\"?", "MessageConfirmRemoveEpisode": "Tem certeza de que deseja remover o episódio \"{0}\"?", @@ -599,7 +599,7 @@ "MessageConfirmReScanLibraryItems": "Tem certeza de que deseja uma nova verificação de {0} itens?", "MessageConfirmSendEbookToDevice": "Tem certeza de que deseja enviar {0} ebook(s) \"{1}\" para o dispositivo \"{2}\"?", "MessageDownloadingEpisode": "Realizando o download do episódio", - "MessageDragFilesIntoTrackOrder": "Arraste os arquivo para ordenar as trilhas corretamente", + "MessageDragFilesIntoTrackOrder": "Arraste os arquivos para ordenar as trilhas corretamente", "MessageEmbedFinished": "Inclusão Concluída!", "MessageEpisodesQueuedForDownload": "{0} Episódio(s) na fila de download", "MessageFeedURLWillBe": "URL do Feed será {0}", @@ -610,14 +610,14 @@ "MessageItemsSelected": "{0} Itens Selecionados", "MessageItemsUpdated": "{0} Itens Atualizados", "MessageJoinUsOn": "Junte-se a nós", - "MessageListeningSessionsInTheLastYear": "{0} sessões de escuta no ano anteiror", + "MessageListeningSessionsInTheLastYear": "{0} sessões de escuta no ano anterior", "MessageLoading": "Carregando...", "MessageLoadingFolders": "Carregando pastas...", "MessageM4BFailed": "Falha no M4B!", "MessageM4BFinished": "M4B Concluído!", "MessageMapChapterTitles": "Designar títulos de capítulos a partir dos capítulos existentes no audiobook sem ajustar seus tempos", - "MessageMarkAllEpisodesFinished": "Marcar todos episódios como concluídos", - "MessageMarkAllEpisodesNotFinished": "Marcar todos episódios como não concluídos", + "MessageMarkAllEpisodesFinished": "Marcar todos os episódios como concluídos", + "MessageMarkAllEpisodesNotFinished": "Marcar todos os episódios como não concluídos", "MessageMarkAsFinished": "Marcar como Concluído", "MessageMarkAsNotFinished": "Marcar como Não Concluído", "MessageMatchBooksDescription": "tentará consultar os livros da biblioteca no fornecedor de busca selecionado e preencher os detalhes ausentes e a capa. Não sobrescreve os detalhes.", @@ -650,7 +650,7 @@ "MessageNoTasksRunning": "Sem Tarefas em Execução", "MessageNotYetImplemented": "Ainda não implementado", "MessageNoUpdateNecessary": "Não é necessária a atualização", - "MessageNoUpdatesWereNecessary": "Nenhuma atualizacão é necessária", + "MessageNoUpdatesWereNecessary": "Nenhuma atualização é necessária", "MessageNoUserPlaylists": "Você não tem listas de reprodução", "MessageOr": "ou", "MessagePauseChapter": "Pausar reprodução do capítulo", @@ -662,7 +662,7 @@ "MessageRemoveEpisodes": "Remover {0} episódio(s)", "MessageRemoveFromPlayerQueue": "Remover da lista do reprodutor", "MessageRemoveUserWarning": "Tem certeza de que deseja apagar definitivamente o usuário \"{0}\"?", - "MessageReportBugsAndContribute": "Reporte bugs, peça funcionalides e contribua em", + "MessageReportBugsAndContribute": "Reporte bugs, peça funcionalidades e contribua em", "MessageResetChaptersConfirm": "Tem certeza de que deseja resetar os capítulos e desfazer as alterações realizadas?", "MessageRestoreBackupConfirm": "Tem certeza de que deseja restaurar o backup criado em", "MessageRestoreBackupWarning": "Restaurar um backup sobrescreverá totalmente o banco de dados localizado em /config e as imagens de capa em /metadata/items & /metadata/authors.<br /><br />Backups não alteram quaisquer arquivos nas pastas da sua biblioteca. Se a configuração do servidor de armazenar a arte da capa e os metadados nas pastas da sua biblioteca estiver ativa, esses itens não estão no backup e não serão sobrescritos.<br /><br />Todos os clientes usando o seu servidor serão atualizados automaticamente.", @@ -673,7 +673,7 @@ "MessageStartPlaybackAtTime": "Iniciar a reprodução de \"{0}\" em {1}?", "MessageThinking": "Pensando...", "MessageUploaderItemFailed": "Falha no upload", - "MessageUploaderItemSuccess": "Uploaded realizado!", + "MessageUploaderItemSuccess": "Upload realizado!", "MessageUploading": "Realizando o upload...", "MessageValidCronExpression": "Expressão do cron válida", "MessageWatcherIsDisabledGlobally": "Monitoramento está desativado nas configurações do servidor", @@ -687,7 +687,7 @@ "NoteRSSFeedPodcastAppsPubDate": "Atenção: Um ou mais dos seus episódios não tem uma data de publicação. Alguns aplicativos de podcasts requerem isto.", "NoteUploaderFoldersWithMediaFiles": "Pastas com arquivos de mídia serão tratadas como itens de biblioteca distintos.", "NoteUploaderOnlyAudioFiles": "Ao subir apenas arquivos de áudio, cada arquivo será tratado como um audiobook distinto.", - "NoteUploaderUnsupportedFiles": "Arquivos não suportados serão ignorados. Ao escolher ou arrastar um pasta, outros arquivos que não estão em uma pasta dentro do item serão ignorados.", + "NoteUploaderUnsupportedFiles": "Arquivos não suportados serão ignorados. Ao escolher ou arrastar uma pasta, outros arquivos que não estão em uma pasta dentro do item serão ignorados.", "PlaceholderNewCollection": "Novo nome da coleção", "PlaceholderNewFolderPath": "Novo caminho para a pasta", "PlaceholderNewPlaylist": "Novo nome da lista de reprodução", @@ -707,7 +707,7 @@ "ToastBackupDeleteSuccess": "Backup apagado", "ToastBackupRestoreFailed": "Falha ao restaurar backup", "ToastBackupUploadFailed": "Falha no upload do backup", - "ToastBackupUploadSuccess": "Uploado do backup realizado", + "ToastBackupUploadSuccess": "Upload do backup realizado", "ToastBatchUpdateFailed": "Falha na atualização em lote", "ToastBatchUpdateSuccess": "Atualização em lote realizada", "ToastBookmarkCreateFailed": "Falha ao criar marcador", @@ -719,7 +719,7 @@ "ToastChaptersHaveErrors": "Capítulos com erro", "ToastChaptersMustHaveTitles": "Capítulos precisam ter títulos", "ToastCollectionItemsRemoveFailed": "Falha ao remover item(ns) da coleção", - "ToastCollectionItemsRemoveSuccess": "Item(ns) removidos da coleçào", + "ToastCollectionItemsRemoveSuccess": "Item(ns) removidos da coleção", "ToastCollectionRemoveFailed": "Falha ao remover coleção", "ToastCollectionRemoveSuccess": "Coleção removida", "ToastCollectionUpdateFailed": "Falha ao atualizar coleção", From 89207b6d2a609653d4f10fdc48ce296e57096fbd Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Fri, 16 Feb 2024 11:57:54 -0300 Subject: [PATCH 0366/2145] Update i18n.js Added PT-BR --- client/plugins/i18n.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index ea6a06db..30f01ab4 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -17,6 +17,7 @@ const languageCodeMap = { 'nl': { label: 'Nederlands', dateFnsLocale: 'nl' }, 'no': { label: 'Norsk', dateFnsLocale: 'no' }, 'pl': { label: 'Polski', dateFnsLocale: 'pl' }, + 'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' }, 'ru': { label: 'Русский', dateFnsLocale: 'ru' }, 'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, From a13217dddffe9eb4259f8e488a30ed485c083f66 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 16 Feb 2024 09:12:47 -0600 Subject: [PATCH 0367/2145] Fix:Initial language code setting eventBus not yet defined --- client/plugins/i18n.js | 2 +- client/plugins/init.client.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 30f01ab4..39eb7a09 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -84,7 +84,7 @@ async function loadi18n(code) { Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale) - this.$eventBus.$emit('change-lang', code) + this?.$eventBus?.$emit('change-lang', code) return true } diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js index a16e6fa1..cbf514fd 100644 --- a/client/plugins/init.client.js +++ b/client/plugins/init.client.js @@ -156,14 +156,14 @@ Vue.prototype.$copyToClipboard = (str, ctx) => { } function xmlToJson(xml) { - const json = {}; + const json = {} for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) { - const key = res[1] || res[3]; - const value = res[2] && xmlToJson(res[2]); - json[key] = ((value && Object.keys(value).length) ? value : res[2]) || null; + const key = res[1] || res[3] + const value = res[2] && xmlToJson(res[2]) + json[key] = ((value && Object.keys(value).length) ? value : res[2]) || null } - return json; + return json } Vue.prototype.$xmlToJson = xmlToJson From 1364b79cbf5aa6807b81e9af8ee8266f898d6add Mon Sep 17 00:00:00 2001 From: ipcintron <ipcintron1@gmail.com> Date: Fri, 16 Feb 2024 10:10:09 -0600 Subject: [PATCH 0368/2145] Put the icon in the link array for iOS only --- client/nuxt.config.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/nuxt.config.js b/client/nuxt.config.js index 85e76325..e5e55974 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -29,7 +29,8 @@ module.exports = { ], script: [], link: [ - { rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' } + { rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' }, + { rel: 'apple-touch-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/icon256.png' } ] }, @@ -110,7 +111,7 @@ module.exports = { sizes: 'any' }, { - src: (process.env.ROUTER_BASE_PATH || '') + '/icon256.png', + src: (process.env.ROUTER_BASE_PATH || '') + '/icon192.png', type: 'image/png', sizes: 'any' } From e180b3c171e2b1f0849d1a983f79c5df5fab32b0 Mon Sep 17 00:00:00 2001 From: ipcintron <ipcintron1@gmail.com> Date: Fri, 16 Feb 2024 12:27:10 -0600 Subject: [PATCH 0369/2145] Renamed the icon to make it clear it is being used for iOS --- client/static/{icon256.png => ios_icon.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename client/static/{icon256.png => ios_icon.png} (100%) diff --git a/client/static/icon256.png b/client/static/ios_icon.png similarity index 100% rename from client/static/icon256.png rename to client/static/ios_icon.png From de6df0c029f05275c16df5b50e82f2657e81a458 Mon Sep 17 00:00:00 2001 From: ipcintron <ipcintron1@gmail.com> Date: Fri, 16 Feb 2024 12:29:52 -0600 Subject: [PATCH 0370/2145] Forgot to modify nuxt.config.js --- client/nuxt.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/nuxt.config.js b/client/nuxt.config.js index e5e55974..6a347c23 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -30,7 +30,7 @@ module.exports = { script: [], link: [ { rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' }, - { rel: 'apple-touch-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/icon256.png' } + { rel: 'apple-touch-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/ios_icon.png' } ] }, From 52323b7eb5a83baa390e3c15cf5f9de9cccb9865 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 16 Feb 2024 16:05:02 -0600 Subject: [PATCH 0371/2145] Update:Podcast episode download show ffmpeg progress and print full debug log dump on error --- server/controllers/MeController.js | 2 +- server/utils/ffmpegHelpers.js | 24 +++++++++++++++++++++--- server/utils/probeWorker.js | 9 --------- 3 files changed, 22 insertions(+), 13 deletions(-) delete mode 100644 server/utils/probeWorker.js diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 8fa5c6bc..b5147fb7 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -336,7 +336,7 @@ class MeController { } /** - * GET: /api/stats/year/:year + * GET: /api/me/stats/year/:year * * @param {import('express').Request} req * @param {import('express').Response} res diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index 3e4848f3..aa0b4805 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -101,8 +101,8 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => { }) if (!response) return resolve(false) - const ffmpeg = Ffmpeg(response.data) + ffmpeg.addOption('-loglevel debug') // Debug logs printed on error ffmpeg.outputOptions( '-c', 'copy', '-metadata', 'podcast=1' @@ -110,6 +110,7 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => { const podcastMetadata = podcastEpisodeDownload.libraryItem.media.metadata const podcastEpisode = podcastEpisodeDownload.podcastEpisode + const finalSizeInBytes = Number(podcastEpisode.enclosure?.length || 0) const taggings = { 'album': podcastMetadata.title, @@ -147,13 +148,30 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => { ffmpeg.addOutput(podcastEpisodeDownload.targetPath) + const stderrLines = [] + ffmpeg.on('stderr', (stderrLine) => { + if (typeof stderrLine === 'string') { + stderrLines.push(stderrLine) + } + }) ffmpeg.on('start', (cmd) => { Logger.debug(`[FfmpegHelpers] downloadPodcastEpisode: Cmd: ${cmd}`) }) - ffmpeg.on('error', (err, stdout, stderr) => { - Logger.error(`[FfmpegHelpers] downloadPodcastEpisode: Error ${err} ${stdout} ${stderr}`) + ffmpeg.on('error', (err) => { + Logger.error(`[FfmpegHelpers] downloadPodcastEpisode: Error ${err}`) + if (stderrLines.length) { + Logger.error(`Full stderr dump for episode url "${podcastEpisodeDownload.url}": ${stderrLines.join('\n')}`) + } resolve(false) }) + ffmpeg.on('progress', (progress) => { + let progressPercent = 0 + if (finalSizeInBytes && progress.targetSize && !isNaN(progress.targetSize)) { + const finalSizeInKb = Math.floor(finalSizeInBytes / 1000) + progressPercent = Math.min(1, progress.targetSize / finalSizeInKb) * 100 + } + Logger.debug(`[FfmpegHelpers] downloadPodcastEpisode: Progress estimate ${progressPercent.toFixed(0)}% (${progress?.targetSize || 'N/A'} KB) for "${podcastEpisodeDownload.url}"`) + }) ffmpeg.on('end', () => { Logger.debug(`[FfmpegHelpers] downloadPodcastEpisode: Complete`) resolve(podcastEpisodeDownload.targetPath) diff --git a/server/utils/probeWorker.js b/server/utils/probeWorker.js deleted file mode 100644 index 2a93fce9..00000000 --- a/server/utils/probeWorker.js +++ /dev/null @@ -1,9 +0,0 @@ -const { parentPort } = require("worker_threads") -const prober = require('./prober') - -parentPort.on("message", async ({ mediaPath }) => { - const results = await prober.probe(mediaPath) - parentPort.postMessage({ - data: results, - }) -}) \ No newline at end of file From aacf37e32b2367906ed9da246af2705f6f5cc9d9 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 16 Feb 2024 16:16:55 -0600 Subject: [PATCH 0372/2145] Fix:Year in Review crashing when listening session has a null genre #2623 --- server/utils/queries/adminStats.js | 2 +- server/utils/queries/userStats.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/utils/queries/adminStats.js b/server/utils/queries/adminStats.js index f1d64f47..0c490de4 100644 --- a/server/utils/queries/adminStats.js +++ b/server/utils/queries/adminStats.js @@ -110,7 +110,7 @@ module.exports = { }) // Filter out bad genres like "audiobook" and "audio book" - const genres = (ls.mediaMetadata.genres || []).filter(g => !g.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book')) + const genres = (ls.mediaMetadata.genres || []).filter(g => g && !g.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book')) genres.forEach((genre) => { if (!genreListeningMap[genre]) genreListeningMap[genre] = 0 genreListeningMap[genre] += (ls.timeListening || 0) diff --git a/server/utils/queries/userStats.js b/server/utils/queries/userStats.js index 6fd5d506..4e4080f8 100644 --- a/server/utils/queries/userStats.js +++ b/server/utils/queries/userStats.js @@ -141,7 +141,7 @@ module.exports = { }) // Filter out bad genres like "audiobook" and "audio book" - const genres = (ls.mediaMetadata.genres || []).filter(g => !g.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book')) + const genres = (ls.mediaMetadata.genres || []).filter(g => g && !g.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book')) genres.forEach((genre) => { if (!genreListeningMap[genre]) genreListeningMap[genre] = 0 genreListeningMap[genre] += listeningSessionListeningTime From 180c328ed1f2aebef661998d2bab1360b88acfc1 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 17 Feb 2024 13:24:49 -0600 Subject: [PATCH 0373/2145] Update jsdocs for search podcasts --- .../pages/library/_library/podcast/search.vue | 9 +++- client/plugins/i18n.js | 4 +- server/controllers/SearchController.js | 4 +- server/finders/PodcastFinder.js | 8 +++- server/providers/iTunes.js | 45 ++++++++++++++++++- 5 files changed, 62 insertions(+), 8 deletions(-) diff --git a/client/pages/library/_library/podcast/search.vue b/client/pages/library/_library/podcast/search.vue index e786562c..4ca3fe7d 100644 --- a/client/pages/library/_library/podcast/search.vue +++ b/client/pages/library/_library/podcast/search.vue @@ -87,7 +87,7 @@ export default { streamLibraryItem() { return this.$store.state.streamLibraryItem }, - librarySetting() { + librarySettings() { return this.$store.getters['libraries/getCurrentLibrarySettings'] } }, @@ -154,7 +154,12 @@ export default { async submitSearch(term) { this.processing = true this.termSearched = '' - let results = await this.$axios.$get(`/api/search/podcast?term=${encodeURIComponent(term)}&country=${encodeURIComponent(this.librarySetting?.podcastSearchRegion)}`).catch((error) => { + + const searchParams = new URLSearchParams({ + term, + country: this.librarySettings?.podcastSearchRegion || 'us' + }) + let results = await this.$axios.$get(`/api/search/podcast?${searchParams.toString()}`).catch((error) => { console.error('Search request failed', error) return [] }) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 67857826..d7fc972e 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -28,9 +28,11 @@ Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { value: code } }) + +// iTunes search API uses ISO 3166 country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 const podcastSearchRegionMap = { 'us': { label: 'United States' }, - 'cn': { label: '中国' }, + 'cn': { label: '中国' } } Vue.prototype.$podcastSearchRegionOptions = Object.keys(podcastSearchRegionMap).map(code => { return { diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index 1acbe926..213d23e1 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -43,14 +43,14 @@ class SearchController { */ async findPodcasts(req, res) { const term = req.query.term - const country = req.query.country + const country = req.query.country || 'us' if (!term) { Logger.error('[SearchController] Invalid request query param "term" is required') return res.status(400).send('Invalid request query param "term" is required') } const results = await PodcastFinder.search(term, { - country: country + country }) res.json(results) } diff --git a/server/finders/PodcastFinder.js b/server/finders/PodcastFinder.js index 52fec15c..abaf02ac 100644 --- a/server/finders/PodcastFinder.js +++ b/server/finders/PodcastFinder.js @@ -6,10 +6,16 @@ class PodcastFinder { this.iTunesApi = new iTunes() } + /** + * + * @param {string} term + * @param {{country:string}} options + * @returns {Promise<import('../providers/iTunes').iTunesPodcastSearchResult[]>} + */ async search(term, options = {}) { if (!term) return null Logger.debug(`[iTunes] Searching for podcast with term "${term}"`) - var results = await this.iTunesApi.searchPodcasts(term, options) + const results = await this.iTunesApi.searchPodcasts(term, options) Logger.debug(`[iTunes] Podcast search for "${term}" returned ${results.length} results`) return results } diff --git a/server/providers/iTunes.js b/server/providers/iTunes.js index 39f36ab2..05a661b5 100644 --- a/server/providers/iTunes.js +++ b/server/providers/iTunes.js @@ -2,16 +2,46 @@ const axios = require('axios') const Logger = require('../Logger') const htmlSanitizer = require('../utils/htmlSanitizer') +/** + * @typedef iTunesSearchParams + * @property {string} term + * @property {string} country + * @property {string} media + * @property {string} entity + * @property {number} limit + */ + +/** + * @typedef iTunesPodcastSearchResult + * @property {string} id + * @property {string} artistId + * @property {string} title + * @property {string} artistName + * @property {string} description + * @property {string} descriptionPlain + * @property {string} releaseDate + * @property {string[]} genres + * @property {string} cover + * @property {string} feedUrl + * @property {string} pageUrl + * @property {boolean} explicit + */ + class iTunes { constructor() { } - // https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/Searching.html + /** + * @see https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/Searching.html + * + * @param {iTunesSearchParams} options + * @returns {Promise<Object[]>} + */ search(options) { if (!options.term) { Logger.error('[iTunes] Invalid search options - no term') return [] } - var query = { + const query = { term: options.term, media: options.media, entity: options.entity, @@ -82,6 +112,11 @@ class iTunes { }) } + /** + * + * @param {Object} data + * @returns {iTunesPodcastSearchResult} + */ cleanPodcast(data) { return { id: data.collectionId, @@ -100,6 +135,12 @@ class iTunes { } } + /** + * + * @param {string} term + * @param {{country:string}} options + * @returns {Promise<iTunesPodcastSearchResult[]>} + */ searchPodcasts(term, options = {}) { return this.search({ term, entity: 'podcast', media: 'podcast', ...options }).then((results) => { return results.map(this.cleanPodcast.bind(this)) From 42a4b762bd9bef33ff55e288f935e4e75c55ff1e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 17 Feb 2024 13:30:30 -0600 Subject: [PATCH 0374/2145] Fix translations sort order and pt-br translations --- client/strings/cs.json | 2 +- 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/pt-br.json | 13 +++++++------ client/strings/ru.json | 2 +- client/strings/sv.json | 2 +- client/strings/zh-cn.json | 2 +- 18 files changed, 24 insertions(+), 23 deletions(-) diff --git a/client/strings/cs.json b/client/strings/cs.json index af8342c8..3ab713ef 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Metoda přehrávání", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasty", + "LabelPodcastSearchRegion": "Oblast vyhledávání podcastu", "LabelPodcastType": "Typ podcastu", "LabelPort": "Port", "LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Vaše záložky", "LabelYourPlaylists": "Vaše seznamy přehrávání", "LabelYourProgress": "Váš pokrok", - "LabelPodcastSearchRegion": "Oblast vyhledávání podcastu", "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.", diff --git a/client/strings/da.json b/client/strings/da.json index 4d9859bc..d456cd51 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Afspilningsmetode", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "Podcast søgeområde", "LabelPodcastType": "Podcast type", "LabelPort": "Port", "LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Dine bogmærker", "LabelYourPlaylists": "Dine spillelister", "LabelYourProgress": "Din fremgang", - "LabelPodcastSearchRegion": "Podcast søgeområde", "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.", diff --git a/client/strings/de.json b/client/strings/de.json index 699f613a..ff8e991a 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Abspielmethode", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "Podcast-Suchregion", "LabelPodcastType": "Podcast Typ", "LabelPort": "Port", "LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Lesezeichen", "LabelYourPlaylists": "Eigene Wiedergabelisten", "LabelYourProgress": "Fortschritt", - "LabelPodcastSearchRegion": "Podcast-Suchregion", "MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen", "MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würdest du <code>http://192.168.1.1:8337/notify</code> eingeben.", "MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index db33d29c..8c58093a 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Play Method", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "Podcast search region", "LabelPodcastType": "Podcast Type", "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Your Bookmarks", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Your Progress", - "LabelPodcastSearchRegion": "Podcast search region", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.", diff --git a/client/strings/es.json b/client/strings/es.json index 3809f2d4..f2714c00 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Método de Reproducción", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "Región de búsqueda de podcasts", "LabelPodcastType": "Tipo Podcast", "LabelPort": "Puerto", "LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Tus Marcadores", "LabelYourPlaylists": "Tus Listas", "LabelYourProgress": "Tu Progreso", - "LabelPodcastSearchRegion": "Región de búsqueda de podcasts", "MessageAddToPlayerQueue": "Agregar a fila del Reproductor", "MessageAppriseDescription": "Para usar esta función deberás tener <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">la API de Apprise</a> corriendo o una API que maneje los mismos resultados. <br/>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 <code>http://192.168.1.1:8337</code> entonces pondría <code>http://192.168.1.1:8337/notify</code>.", "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 <code>/metadata/items</code> y <code>/metadata/authors</code>. Los Respaldos <strong>NO</strong> incluyen ningún archivo guardado en la carpeta de tu biblioteca.", diff --git a/client/strings/fr.json b/client/strings/fr.json index 59b7288d..3f04fab8 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Méthode d’écoute", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "Région de recherche de podcasts", "LabelPodcastType": "Type de Podcast", "LabelPort": "Port", "LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Vos signets", "LabelYourPlaylists": "Vos listes de lecture", "LabelYourProgress": "Votre progression", - "LabelPodcastSearchRegion": "Région de recherche de podcasts", "MessageAddToPlayerQueue": "Ajouter en file d’attente", "MessageAppriseDescription": "Nécessite une instance d’<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes.<br>L’URL de l’API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les sauvegardes n’incluent pas les fichiers de votre bibliothèque.", diff --git a/client/strings/gu.json b/client/strings/gu.json index 6ee3666d..daa923db 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Play Method", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "પોડકાસ્ટ શોધ પ્રદેશ", "LabelPodcastType": "Podcast Type", "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Your Bookmarks", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Your Progress", - "LabelPodcastSearchRegion": "પોડકાસ્ટ શોધ પ્રદેશ", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.", diff --git a/client/strings/hi.json b/client/strings/hi.json index db49cc37..a59b43ec 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Play Method", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "पॉडकास्ट खोज क्षेत्र", "LabelPodcastType": "Podcast Type", "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Your Bookmarks", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Your Progress", - "LabelPodcastSearchRegion": "पॉडकास्ट खोज क्षेत्र", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.", diff --git a/client/strings/hr.json b/client/strings/hr.json index d7b32b70..6e105ca2 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Vrsta reprodukcije", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "Područje pretrage podcasta", "LabelPodcastType": "Podcast Type", "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Tvoje knjižne oznake", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Tvoj napredak", - "LabelPodcastSearchRegion": "Područje pretrage podcasta", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Backups uključuju korisnike, korisnikov napredak, detalje stavki iz biblioteke, postavke server i slike iz <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups ne uključuju nijedne datoteke koje su u folderima biblioteke.", diff --git a/client/strings/it.json b/client/strings/it.json index eb514f69..53394481 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Metodo di riproduzione", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "Area di ricerca podcast", "LabelPodcastType": "Tipo di Podcast", "LabelPort": "Port", "LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "I tuoi Preferiti", "LabelYourPlaylists": "le tue Playlist", "LabelYourProgress": "Completato al", - "LabelPodcastSearchRegion": "Area di ricerca podcast", "MessageAddToPlayerQueue": "Aggiungi alla coda di riproduzione", "MessageAppriseDescription": "Per utilizzare questa funzione è necessario disporre di un'istanza di <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> in esecuzione o un'API che gestirà quelle stesse richieste. <br />L'API Url dovrebbe essere il percorso URL completo per inviare la notifica, ad esempio se la tua istanza API è servita cosi .<code>http://192.168.1.1:8337</code> Allora dovrai mettere <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "I backup includono utenti, progressi degli utenti, dettagli sugli elementi della libreria, impostazioni del server e immagini archiviate in <code>/metadata/items</code> & <code>/metadata/authors</code>. I backup non includono i file archiviati nelle cartelle della libreria.", diff --git a/client/strings/lt.json b/client/strings/lt.json index bbcd02cd..f98d5aee 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Grojimo metodas", "LabelPodcast": "Tinklalaidė", "LabelPodcasts": "Tinklalaidės", + "LabelPodcastSearchRegion": "Podcast paieškos regionas", "LabelPodcastType": "Tinklalaidės tipas", "LabelPort": "Prievadas", "LabelPrefixesToIgnore": "Ignoruojami priešdėliai (didžiosios/mažosios nesvarbu)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Jūsų skirtukai", "LabelYourPlaylists": "Jūsų grojaraščiai", "LabelYourProgress": "Jūsų pažanga", - "LabelPodcastSearchRegion": "Podcast paieškos regionas", "MessageAddToPlayerQueue": "Pridėti į grotuvo eilę", "MessageAppriseDescription": "Norint naudoti šią funkciją, reikės turėti <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> veikiantį arba API, kuris tvarkys tas pačias užklausas.<br />Apprise API URL turėtų būti visi kelio takai iki pranešimo siuntimo, pvz., jei jūsų API pasiekiamas adresu <code>http://192.168.1.1:8337</code>, tada įveskite <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Atsarginės kopijos apima vartotojus, vartotojų pažangą, bibliotekos elemento informaciją, serverio nustatymus ir vaizdus, saugomus <code>/metadata/items</code> ir <code>/metadata/authors</code>. Atsarginės kopijos <strong>neįtraukia</strong> jokių failų, saugomų jūsų bibliotekos aplankuose.", diff --git a/client/strings/nl.json b/client/strings/nl.json index 4cda655c..1c1ffa1b 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Afspeelwijze", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "Podcast zoekregio", "LabelPodcastType": "Podcasttype", "LabelPort": "Poort", "LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Je boekwijzers", "LabelYourPlaylists": "Je afspeellijsten", "LabelYourProgress": "Je voortgang", - "LabelPodcastSearchRegion": "Podcast zoekregio", "MessageAddToPlayerQueue": "Toevoegen aan wachtrij", "MessageAppriseDescription": "Om deze functie te gebruiken heb je een draaiende instantie van <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nodig of een api die dezelfde requests afhandelt. <br />De Apprise API Url moet het volledige URL-pad zijn om de notificatie te verzenden, b.v., als je API-instantie draait op <code>http://192.168.1.1:8337</code> dan zou je <code>http://192.168.1.1:8337/notify</code> gebruiken.", "MessageBackupsDescription": "Back-ups omvatten gebruikers, gebruikers' voortgang, bibliotheekonderdeeldetails, serverinstellingen en afbeeldingen bewaard in <code>/metadata/items</code> & <code>/metadata/authors</code>. Back-ups <strong>bevatten niet</strong> de bestanden bewaard in je bibliotheekmappen.", diff --git a/client/strings/no.json b/client/strings/no.json index 9ced1167..432115df 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Avspillingsmetode", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcaster", + "LabelPodcastSearchRegion": "Podcast-søkeområde", "LabelPodcastType": "Podcast type", "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefiks som skal ignoreres (skiller ikke mellom store og små bokstaver)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Dine bokmerker", "LabelYourPlaylists": "Dine spillelister", "LabelYourProgress": "Din fremgang", - "LabelPodcastSearchRegion": "Podcast-søkeområde", "MessageAddToPlayerQueue": "Legg til i kø", "MessageAppriseDescription": "For å bruke denne funksjonen trenger du en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> kjørende eller ett api som vil håndere disse forespørslene. <br />Apprise API Url skal være den fulle URL stien for å sende Notifikasjonen, f.eks., hvis din API instans er hos <code>http://192.168.1.1:8337</code> vil du bruke <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Sikkerhetskopier inkluderer, brukerfremgang, detaljer om bibliotekgjenstander, tjener instillinger og bilder lagret under <code>/metadata/items</code> og <code>/metadata/authors</code>. Sikkerhetskopier <strong>vil ikke</strong> inkludere filer som er lagret i bibliotek mappene.", diff --git a/client/strings/pl.json b/client/strings/pl.json index 0fca2842..7f0e4497 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Metoda odtwarzania", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasty", + "LabelPodcastSearchRegion": "Obszar wyszukiwania podcastów", "LabelPodcastType": "Podcast Type", "LabelPort": "Port", "LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Twoje zakładki", "LabelYourPlaylists": "Your Playlists", "LabelYourProgress": "Twój postęp", - "LabelPodcastSearchRegion": "Obszar wyszukiwania podcastów", "MessageAddToPlayerQueue": "Add to player queue", "MessageAppriseDescription": "Aby użyć tej funkcji, konieczne jest posiadanie instancji <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> albo innego rozwiązania, które obsługuje schemat zapytań Apprise. <br />URL do interfejsu API powinno być całkowitą ścieżką, np., jeśli Twoje API do powiadomień jest dostępne pod adresem <code>http://192.168.1.1:8337</code> to wpisany tutaj URL powinien mieć postać: <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Kopie zapasowe obejmują użytkowników, postępy użytkowników, szczegóły pozycji biblioteki, ustawienia serwera i obrazy przechowywane w <code>/metadata/items</code> & <code>/metadata/authors</code>. Kopie zapasowe nie obejmują żadnych plików przechowywanych w folderach biblioteki.", diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 148d53d5..cd4e9cab 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -40,8 +40,8 @@ "ButtonLookup": "Procurar", "ButtonManageTracks": "Gerenciar Faixas", "ButtonMapChapterTitles": "Designar Títulos de Capítulos", - "ButtonitensAllAuthors": "Consultar Todos os Autores", - "ButtonitensBooks": "Consultar Livros", + "ButtonMatchAllAuthors": "Consultar Todos os Autores", + "ButtonMatchBooks": "Consultar Livros", "ButtonNevermind": "Cancelar", "ButtonNextChapter": "Próximo Capítulo", "ButtonOk": "Ok", @@ -57,7 +57,7 @@ "ButtonPurgeMediaProgress": "Apagar o Progresso nas Mídias", "ButtonQueueAddItem": "Adicionar à Lista", "ButtonQueueRemoveItem": "Remover da Lista", - "ButtonQuickitens": "Consulta rápida", + "ButtonQuickMatch": "Consulta rápida", "ButtonRead": "Ler", "ButtonRemove": "Remover", "ButtonRemoveAll": "Remover Todos", @@ -133,9 +133,9 @@ "HeaderLogin": "Login", "HeaderLogs": "Logs", "HeaderManageGenres": "Gerenciar Gêneros", - "HeaderManageetiquetas": "Gerenciar Etiquetas", + "HeaderManageTags": "Gerenciar Etiquetas", "HeaderMapDetails": "Designar Detalhes", - "Headeritens": "Consultar", + "HeaderMatch": "Consultar", "HeaderMetadataOrderOfPrecedence": "Ordem de Prioridade dos Metadados", "HeaderMetadataToEmbed": "Metadados a Serem Incluídos", "HeaderNewAccount": "Nova Conta", @@ -396,6 +396,7 @@ "LabelPlayMethod": "Método de Reprodução", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "Podcast search region", "LabelPodcastType": "Tipo de Podcast", "LabelPort": "Porta", "LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)", @@ -764,4 +765,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} +} \ No newline at end of file diff --git a/client/strings/ru.json b/client/strings/ru.json index 7dc528d6..3657b596 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Метод воспроизведения", "LabelPodcast": "Подкаст", "LabelPodcasts": "Подкасты", + "LabelPodcastSearchRegion": "Регион поиска подкастов", "LabelPodcastType": "Тип подкаста", "LabelPort": "Порт", "LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Ваши закладки", "LabelYourPlaylists": "Ваши плейлисты", "LabelYourProgress": "Ваш прогресс", - "LabelPodcastSearchRegion": "Регион поиска подкастов", "MessageAddToPlayerQueue": "Добавить в очередь проигрывателя", "MessageAppriseDescription": "Для использования этой функции необходимо иметь запущенный экземпляр <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или api которое обрабатывает те же самые запросы. <br />URL-адрес API Apprise должен быть полным URL-адресом для отправки уведомления, т.е., если API запущено по адресу <code>http://192.168.1.1:8337</code> тогда нужно указать <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "Бэкап включает пользователей, прогресс пользователей, данные элементов библиотеки, настройки сервера и изображения хранящиеся в <code>/metadata/items</code> и <code>/metadata/authors</code>. Бэкапы <strong>НЕ</strong> сохраняют файлы из папок библиотек.", diff --git a/client/strings/sv.json b/client/strings/sv.json index 36b4964f..986f2c4b 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "Spelläge", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", + "LabelPodcastSearchRegion": "Podcast-sökområde", "LabelPodcastType": "Podcasttyp", "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "Dina bokmärken", "LabelYourPlaylists": "Dina spellistor", "LabelYourProgress": "Din framsteg", - "LabelPodcastSearchRegion": "Podcast-sökområde", "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.", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index c5ec502a..fd7fe290 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -396,6 +396,7 @@ "LabelPlayMethod": "播放方法", "LabelPodcast": "播客", "LabelPodcasts": "播客", + "LabelPodcastSearchRegion": "播客搜索地区", "LabelPodcastType": "播客类型", "LabelPort": "端口", "LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)", @@ -555,7 +556,6 @@ "LabelYourBookmarks": "你的书签", "LabelYourPlaylists": "你的播放列表", "LabelYourProgress": "你的进度", - "LabelPodcastSearchRegion": "播客搜索地区", "MessageAddToPlayerQueue": "添加到播放队列", "MessageAppriseDescription": "要使用此功能,您需要运行一个 <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> 实例或一个可以处理这些相同请求的 API. <br />Apprise API Url 应该是发送通知的完整 URL 路径, 例如: 如果你的 API 实例运行在 <code>http://192.168.1.1:8337</code>, 那么你可以输入 <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "备份包括用户, 用户进度, 媒体库项目详细信息, 服务器设置和图像, 存储在 <code>/metadata/items</code> & <code>/metadata/authors</code>. 备份不包括存储在您的媒体库文件夹中的任何文件.", From d7aba5629e0cdbb61be283c88b4e6d9f97c5e59b Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 17 Feb 2024 15:29:06 -0600 Subject: [PATCH 0375/2145] Remove old login rate limiter --- server/Server.js | 27 --- server/libs/expressRateLimit/LICENSE | 20 -- server/libs/expressRateLimit/index.js | 196 ------------------- server/libs/expressRateLimit/memory-store.js | 47 ----- 4 files changed, 290 deletions(-) delete mode 100644 server/libs/expressRateLimit/LICENSE delete mode 100644 server/libs/expressRateLimit/index.js delete mode 100644 server/libs/expressRateLimit/memory-store.js diff --git a/server/Server.js b/server/Server.js index 6feabee8..01b1af12 100644 --- a/server/Server.js +++ b/server/Server.js @@ -5,7 +5,6 @@ const http = require('http') const util = require('util') 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') @@ -287,8 +286,6 @@ class Server { ] dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html')))) - // 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`) @@ -401,30 +398,6 @@ class Server { } } - // First time login rate limit is hit - loginLimitReached(req, res, options) { - Logger.error(`[Server] Login rate limit (${options.max}) was hit for ip ${req.ip}`) - options.message = 'Too many attempts. Login temporarily locked.' - } - - getLoginRateLimiter() { - return rateLimit({ - windowMs: Database.serverSettings.rateLimitLoginWindow, // 5 minutes - max: Database.serverSettings.rateLimitLoginRequests, - skipSuccessfulRequests: true, - onLimitReached: this.loginLimitReached - }) - } - - logout(req, res) { - if (req.body.socketId) { - Logger.info(`[Server] User ${req.user ? req.user.username : 'Unknown'} is logging out with socket ${req.body.socketId}`) - SocketAuthority.logout(req.body.socketId) - } - - res.sendStatus(200) - } - /** * Gracefully stop server * Stops watcher and socket server diff --git a/server/libs/expressRateLimit/LICENSE b/server/libs/expressRateLimit/LICENSE deleted file mode 100644 index f4bb9cc3..00000000 --- a/server/libs/expressRateLimit/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -# MIT License - -Copyright 2021 Nathan Friedly - -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. \ No newline at end of file diff --git a/server/libs/expressRateLimit/index.js b/server/libs/expressRateLimit/index.js deleted file mode 100644 index 6df27ff5..00000000 --- a/server/libs/expressRateLimit/index.js +++ /dev/null @@ -1,196 +0,0 @@ -"use strict"; - -// -// modified for use in audiobookshelf -// Source: https://github.com/nfriedly/express-rate-limit -// - -const MemoryStore = require("./memory-store"); - -function RateLimit(options) { - options = Object.assign( - { - windowMs: 60 * 1000, // milliseconds - how long to keep records of requests in memory - max: 5, // max number of recent connections during `window` milliseconds before sending a 429 response - message: "Too many requests, please try again later.", - statusCode: 429, // 429 status = Too Many Requests (RFC 6585) - headers: true, //Send custom rate limit header with limit and remaining - draft_polli_ratelimit_headers: false, //Support for the new RateLimit standardization headers - // ability to manually decide if request was successful. Used when `skipSuccessfulRequests` and/or `skipFailedRequests` are set to `true` - requestWasSuccessful: function (req, res) { - return res.statusCode < 400; - }, - skipFailedRequests: false, // Do not count failed requests - skipSuccessfulRequests: false, // Do not count successful requests - // allows to create custom keys (by default user IP is used) - keyGenerator: function (req /*, res*/) { - if (!req.ip) { - console.error( - "express-rate-limit: req.ip is undefined - you can avoid this by providing a custom keyGenerator function, but it may be indicative of a larger issue." - ); - } - return req.ip; - }, - skip: function (/*req, res*/) { - return false; - }, - handler: function (req, res /*, next, optionsUsed*/) { - res.status(options.statusCode).send(options.message); - }, - onLimitReached: function (/*req, res, optionsUsed*/) { }, - requestPropertyName: "rateLimit", // Parameter name appended to req object - }, - options - ); - - // store to use for persisting rate limit data - options.store = options.store || new MemoryStore(options.windowMs); - - // ensure that the store has the incr method - if ( - typeof options.store.incr !== "function" || - typeof options.store.resetKey !== "function" || - (options.skipFailedRequests && - typeof options.store.decrement !== "function") - ) { - throw new Error("The store is not valid."); - } - - ["global", "delayMs", "delayAfter"].forEach((key) => { - // note: this doesn't trigger if delayMs or delayAfter are set to 0, because that essentially disables them - if (options[key]) { - throw new Error( - `The ${key} option was removed from express-rate-limit v3.` - ); - } - }); - - function rateLimit(req, res, next) { - Promise.resolve(options.skip(req, res)) - .then((skip) => { - if (skip) { - return next(); - } - - const key = options.keyGenerator(req, res); - - options.store.incr(key, function (err, current, resetTime) { - if (err) { - return next(err); - } - - const maxResult = - typeof options.max === "function" - ? options.max(req, res) - : options.max; - - Promise.resolve(maxResult) - .then((max) => { - req[options.requestPropertyName] = { - limit: max, - current: current, - remaining: Math.max(max - current, 0), - resetTime: resetTime, - }; - - if (options.headers && !res.headersSent) { - res.setHeader("X-RateLimit-Limit", max); - res.setHeader( - "X-RateLimit-Remaining", - req[options.requestPropertyName].remaining - ); - if (resetTime instanceof Date) { - // if we have a resetTime, also provide the current date to help avoid issues with incorrect clocks - res.setHeader("Date", new Date().toUTCString()); - res.setHeader( - "X-RateLimit-Reset", - Math.ceil(resetTime.getTime() / 1000) - ); - } - } - if (options.draft_polli_ratelimit_headers && !res.headersSent) { - res.setHeader("RateLimit-Limit", max); - res.setHeader( - "RateLimit-Remaining", - req[options.requestPropertyName].remaining - ); - if (resetTime) { - const deltaSeconds = Math.ceil( - (resetTime.getTime() - Date.now()) / 1000 - ); - res.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds)); - } - } - - if ( - options.skipFailedRequests || - options.skipSuccessfulRequests - ) { - let decremented = false; - const decrementKey = () => { - if (!decremented) { - options.store.decrement(key); - decremented = true; - } - }; - - if (options.skipFailedRequests) { - res.on("finish", function () { - if (!options.requestWasSuccessful(req, res)) { - decrementKey(); - } - }); - - res.on("close", () => { - if (!res.finished) { - decrementKey(); - } - }); - - res.on("error", () => decrementKey()); - } - - if (options.skipSuccessfulRequests) { - res.on("finish", function () { - if (options.requestWasSuccessful(req, res)) { - options.store.decrement(key); - } - }); - } - } - - if (max && current === max + 1) { - options.onLimitReached(req, res, options); - } - - if (max && current > max) { - if (options.headers && !res.headersSent) { - res.setHeader( - "Retry-After", - Math.ceil(options.windowMs / 1000) - ); - } - return options.handler(req, res, next, options); - } - - next(); - - return null; - }) - .catch(next); - }); - - return null; - }) - .catch(next); - } - - rateLimit.resetKey = options.store.resetKey.bind(options.store); - - // Backward compatibility function - rateLimit.resetIp = rateLimit.resetKey; - - return rateLimit; -} - -module.exports = RateLimit; diff --git a/server/libs/expressRateLimit/memory-store.js b/server/libs/expressRateLimit/memory-store.js deleted file mode 100644 index 60938dbc..00000000 --- a/server/libs/expressRateLimit/memory-store.js +++ /dev/null @@ -1,47 +0,0 @@ -"use strict"; - -function calculateNextResetTime(windowMs) { - const d = new Date(); - d.setMilliseconds(d.getMilliseconds() + windowMs); - return d; -} - -function MemoryStore(windowMs) { - let hits = {}; - let resetTime = calculateNextResetTime(windowMs); - - this.incr = function (key, cb) { - if (hits[key]) { - hits[key]++; - } else { - hits[key] = 1; - } - - cb(null, hits[key], resetTime); - }; - - this.decrement = function (key) { - if (hits[key]) { - hits[key]--; - } - }; - - // export an API to allow hits all IPs to be reset - this.resetAll = function () { - hits = {}; - resetTime = calculateNextResetTime(windowMs); - }; - - // export an API to allow hits from one IP to be reset - this.resetKey = function (key) { - delete hits[key]; - }; - - // simply reset ALL hits every windowMs - const interval = setInterval(this.resetAll, windowMs); - if (interval.unref) { - interval.unref(); - } -} - -module.exports = MemoryStore; From bf66e13377111c89b22f1d64296fe3065747d078 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 17 Feb 2024 16:06:25 -0600 Subject: [PATCH 0376/2145] Update jsdocs --- server/Auth.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 9dfe2416..cec3bc33 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -656,7 +656,7 @@ class Auth { * Checks if a username and password tuple is valid and the user active. * @param {string} username * @param {string} password - * @param {function} done + * @param {Promise<function>} done */ async localAuthCheckUserPw(username, password, done) { // Load the user given it's username @@ -698,7 +698,7 @@ class Auth { /** * Hashes a password with bcrypt. * @param {string} password - * @returns {string} hash + * @returns {Promise<string>} hash */ hashPass(password) { return new Promise((resolve) => { @@ -732,8 +732,8 @@ class Auth { /** * * @param {string} password - * @param {*} user - * @returns {boolean} + * @param {import('./models/User')} user + * @returns {Promise<boolean>} */ comparePassword(password, user) { if (user.type === 'root' && !password && !user.pash) return true From e47ea98cdd1843752862f99d6678a1305cb5b6d7 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 17 Feb 2024 16:58:49 -0600 Subject: [PATCH 0377/2145] Fix:Disconnect from socket on logout, remove unnecessary logout function --- client/pages/account.vue | 31 ++++++++++++++++++------------- server/SocketAuthority.js | 19 ------------------- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/client/pages/account.vue b/client/pages/account.vue index ba5370c3..f531a34d 100644 --- a/client/pages/account.vue +++ b/client/pages/account.vue @@ -82,9 +82,11 @@ export default { this.$setLanguageCode(lang) }, logout() { - var rootSocket = this.$root.socket || {} - const logoutPayload = { - socketId: rootSocket.id + // Disconnect from socket + if (this.$root.socket) { + console.log('Disconnecting from socket', this.$root.socket.id) + this.$root.socket.removeAllListeners() + this.$root.socket.disconnect() } if (localStorage.getItem('token')) { @@ -93,17 +95,20 @@ export default { this.$store.commit('libraries/setUserPlaylists', []) this.$store.commit('libraries/setCollections', []) - this.$axios.$post('/logout').then((logoutPayload) => { - const redirect_url = logoutPayload.redirect_url + this.$axios + .$post('/logout') + .then((logoutPayload) => { + const redirect_url = logoutPayload.redirect_url - if (redirect_url) { - window.location.href = redirect_url - } else { - this.$router.push('/login') - } - }).catch((error) => { - console.error(error) - }) + if (redirect_url) { + window.location.href = redirect_url + } else { + this.$router.push('/login') + } + }) + .catch((error) => { + console.error(error) + }) }, resetForm() { this.password = null diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index b626c0e4..930037a8 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -219,25 +219,6 @@ class SocketAuthority { client.socket.emit('init', initialPayload) } - logout(socketId) { - // Strip user and client from client and client socket - if (socketId && this.clients[socketId]) { - const client = this.clients[socketId] - const clientSocket = client.socket - Logger.debug(`[SocketAuthority] Found user client ${clientSocket.id}, Has user: ${!!client.user}, Socket has client: ${!!clientSocket.sheepClient}`) - - if (client.user) { - Logger.debug('[SocketAuthority] User Offline ' + client.user.username) - this.adminEmitter('user_offline', client.user.toJSONForPublic()) - } - - delete this.clients[socketId].user - if (clientSocket && clientSocket.sheepClient) delete this.clients[socketId].socket.sheepClient - } else if (socketId) { - Logger.warn(`[SocketAuthority] No client for socket ${socketId}`) - } - } - cancelScan(id) { Logger.debug('[SocketAuthority] Cancel scan', id) this.Server.cancelLibraryScan(id) From d9e7f5d1333fb9abd39d9b368e2236bf716c0acd Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 17 Feb 2024 17:40:33 -0600 Subject: [PATCH 0378/2145] Update BinaryManager JSDocs, move validVersions to required binary objects --- server/managers/BinaryManager.js | 80 +++++++++++++++++++++++++------- server/utils/fileUtils.js | 2 +- 2 files changed, 63 insertions(+), 19 deletions(-) diff --git a/server/managers/BinaryManager.js b/server/managers/BinaryManager.js index 1a898236..ec4ed3b6 100644 --- a/server/managers/BinaryManager.js +++ b/server/managers/BinaryManager.js @@ -11,16 +11,15 @@ const fileUtils = require('../utils/fileUtils') class BinaryManager { defaultRequiredBinaries = [ - { name: 'ffmpeg', envVariable: 'FFMPEG_PATH' }, - { name: 'ffprobe', envVariable: 'FFPROBE_PATH' } + { name: 'ffmpeg', envVariable: 'FFMPEG_PATH', validVersions: ['5.1', '6'] }, + { name: 'ffprobe', envVariable: 'FFPROBE_PATH', validVersions: ['5.1', '6'] } ] - goodVersions = [ '5.1', '6' ] - constructor(requiredBinaries = this.defaultRequiredBinaries) { this.requiredBinaries = requiredBinaries this.mainInstallPath = process.pkg ? path.dirname(process.execPath) : global.appRoot this.altInstallPath = global.ConfigPath + this.initialized = false this.exec = exec } @@ -31,30 +30,45 @@ class BinaryManager { await this.removeOldBinaries(missingBinaries) await this.install(missingBinaries) const missingBinariesAfterInstall = await this.findRequiredBinaries() - if (missingBinariesAfterInstall.length != 0) { + if (missingBinariesAfterInstall.length) { Logger.error(`[BinaryManager] Failed to find or install required binaries: ${missingBinariesAfterInstall.join(', ')}`) process.exit(1) } this.initialized = true } + /** + * Remove old/invalid binaries in main or alt install path + * + * @param {string[]} binaryNames + */ async removeOldBinaries(binaryNames) { for (const binaryName of binaryNames) { const executable = this.getExecutableFileName(binaryName) const mainInstallPath = path.join(this.mainInstallPath, executable) + if (await fs.pathExists(mainInstallPath)) { + Logger.debug(`[BinaryManager] Removing old binary: ${mainInstallPath}`) + await fs.remove(mainInstallPath) + } const altInstallPath = path.join(this.altInstallPath, executable) - Logger.debug(`[BinaryManager] Removing old binaries: ${mainInstallPath}, ${altInstallPath}`) - await fs.remove(mainInstallPath) - await fs.remove(altInstallPath) + if (await fs.pathExists(altInstallPath)) { + Logger.debug(`[BinaryManager] Removing old binary: ${altInstallPath}`) + await fs.remove(altInstallPath) + } } } + /** + * Find required binaries and return array of binary names that are missing + * + * @returns {Promise<string[]>} + */ async findRequiredBinaries() { const missingBinaries = [] for (const binary of this.requiredBinaries) { - const binaryPath = await this.findBinary(binary.name, binary.envVariable) + const binaryPath = await this.findBinary(binary.name, binary.envVariable, binary.validVersions) if (binaryPath) { - Logger.info(`[BinaryManager] Found good ${binary.name} at ${binaryPath}`) + Logger.info(`[BinaryManager] Found valid binary ${binary.name} at ${binaryPath}`) if (process.env[binary.envVariable] !== binaryPath) { Logger.info(`[BinaryManager] Updating process.env.${binary.envVariable}`) process.env[binary.envVariable] = binaryPath @@ -67,40 +81,70 @@ class BinaryManager { return missingBinaries } - async findBinary(name, envVariable) { + /** + * Find absolute path for binary + * + * @param {string} name + * @param {string} envVariable + * @param {string[]} [validVersions] + * @returns {Promise<string>} Path to binary + */ + async findBinary(name, envVariable, validVersions = []) { const executable = this.getExecutableFileName(name) + // 1. check path specified in environment variable const defaultPath = process.env[envVariable] - if (await this.isBinaryGood(defaultPath)) return defaultPath + if (await this.isBinaryGood(defaultPath, validVersions)) return defaultPath + // 2. find the first instance of the binary in the PATH environment variable const whichPath = which.sync(executable, { nothrow: true }) - if (await this.isBinaryGood(whichPath)) return whichPath + if (await this.isBinaryGood(whichPath, validVersions)) return whichPath + // 3. check main install path (binary root dir) const mainInstallPath = path.join(this.mainInstallPath, executable) - if (await this.isBinaryGood(mainInstallPath)) return mainInstallPath + if (await this.isBinaryGood(mainInstallPath, validVersions)) return mainInstallPath + // 4. check alt install path (/config) const altInstallPath = path.join(this.altInstallPath, executable) - if (await this.isBinaryGood(altInstallPath)) return altInstallPath + if (await this.isBinaryGood(altInstallPath, validVersions)) return altInstallPath return null } - async isBinaryGood(binaryPath) { + /** + * Check binary path exists and optionally check version is valid + * + * @param {string} binaryPath + * @param {string[]} [validVersions] + * @returns {Promise<boolean>} + */ + async isBinaryGood(binaryPath, validVersions = []) { if (!binaryPath || !await fs.pathExists(binaryPath)) return false + if (!validVersions.length) return true try { const { stdout } = await this.exec('"' + binaryPath + '"' + ' -version') const version = stdout.match(/version\s([\d\.]+)/)?.[1] if (!version) return false - return this.goodVersions.some(goodVersion => version.startsWith(goodVersion)) + return validVersions.some(validVersion => version.startsWith(validVersion)) } catch (err) { Logger.error(`[BinaryManager] Failed to check version of ${binaryPath}`) return false } } + /** + * + * @param {string[]} binaries + */ async install(binaries) { - if (binaries.length == 0) return + if (!binaries.length) return Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`) let destination = await fileUtils.isWritable(this.mainInstallPath) ? this.mainInstallPath : this.altInstallPath await ffbinaries.downloadBinaries(binaries, { destination, version: '6.1', force: true }) Logger.info(`[BinaryManager] Binaries installed to ${destination}`) } + /** + * Append .exe to binary name for Windows + * + * @param {string} name + * @returns {string} + */ getExecutableFileName(name) { return name + (process.platform == 'win32' ? '.exe' : '') } diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index b01a186c..99bb49eb 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -366,7 +366,7 @@ module.exports.encodeUriPath = (path) => { * This method is necessary because fs.access(directory, fs.constants.W_OK) does not work on Windows * * @param {string} directory - * @returns {boolean} + * @returns {Promise<boolean>} */ module.exports.isWritable = async (directory) => { try { From 7a570439db113b43d282827b1fa95044f43bd267 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 18 Feb 2024 11:24:36 -0600 Subject: [PATCH 0379/2145] Update:Clamp item descriptions to 4 lines and show more button #2614 --- client/assets/app.css | 30 ------------------ .../components/modals/podcast/EpisodeFeed.vue | 2 +- .../modals/podcast/tabs/EpisodeMatch.vue | 2 +- .../tables/podcast/LazyEpisodeRow.vue | 4 +-- client/pages/item/_id/index.vue | 31 +++++++++++++++++-- .../pages/library/_library/podcast/latest.vue | 2 +- 6 files changed, 34 insertions(+), 37 deletions(-) diff --git a/client/assets/app.css b/client/assets/app.css index 1a83dc1c..2e0714f9 100644 --- a/client/assets/app.css +++ b/client/assets/app.css @@ -217,36 +217,6 @@ Bookshelf Label filter: blur(20px); } - -.episode-subtitle { - word-break: break-word; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - line-height: 16px; - /* fallback */ - max-height: 32px; - /* fallback */ - -webkit-line-clamp: 2; - /* number of lines to show */ - -webkit-box-orient: vertical; -} - -.episode-subtitle-long { - word-break: break-word; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - line-height: 16px; - /* fallback */ - max-height: 72px; - /* fallback */ - -webkit-line-clamp: 6; - /* number of lines to show */ - -webkit-box-orient: vertical; -} - - /* Padding for toastification toasts in the top right to not cover appbar/toolbar */ .app-bar-and-toolbar .Vue-Toastification__container.top-right { padding-top: 104px; diff --git a/client/components/modals/podcast/EpisodeFeed.vue b/client/components/modals/podcast/EpisodeFeed.vue index b5d98a25..8e4c1826 100644 --- a/client/components/modals/podcast/EpisodeFeed.vue +++ b/client/components/modals/podcast/EpisodeFeed.vue @@ -33,7 +33,7 @@ <div class="break-words">{{ episode.title }}</div> <widgets-podcast-type-indicator :type="episode.episodeType" /> </div> - <p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p> + <p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p> <p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p> </div> </div> diff --git a/client/components/modals/podcast/tabs/EpisodeMatch.vue b/client/components/modals/podcast/tabs/EpisodeMatch.vue index 0603a9a8..640ec547 100644 --- a/client/components/modals/podcast/tabs/EpisodeMatch.vue +++ b/client/components/modals/podcast/tabs/EpisodeMatch.vue @@ -18,7 +18,7 @@ <div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)"> <p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p> <p class="break-words mb-1">{{ episode.title }}</p> - <p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p> + <p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p> <p class="text-xs text-gray-400">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p> </div> </template> diff --git a/client/components/tables/podcast/LazyEpisodeRow.vue b/client/components/tables/podcast/LazyEpisodeRow.vue index fecf7758..18576340 100644 --- a/client/components/tables/podcast/LazyEpisodeRow.vue +++ b/client/components/tables/podcast/LazyEpisodeRow.vue @@ -7,8 +7,8 @@ <widgets-podcast-type-indicator :type="episodeType" /> </div> - <div class="h-10 flex items-center mt-1.5 mb-0.5"> - <p class="text-sm text-gray-200 episode-subtitle" v-html="episodeSubtitle"></p> + <div class="h-10 flex items-center mt-1.5 mb-0.5 overflow-hidden"> + <p class="text-sm text-gray-200 line-clamp-2" v-html="episodeSubtitle"></p> </div> <div class="h-8 flex items-center"> <div class="w-full inline-flex justify-between max-w-xl"> diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 073ec570..d568d534 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -125,7 +125,10 @@ </div> <div class="my-4 w-full"> - <p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p> + <p ref="description" id="item-description" class="text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }">{{ description }}</p> + <button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription"> + {{ showFullDescription ? 'Read less' : 'Read more' }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span> + </button> </div> <div v-if="invalidAudioFiles.length" class="bg-error border-red-800 shadow-md p-4"> @@ -182,7 +185,9 @@ export default { podcastFeedEpisodes: [], episodesDownloading: [], episodeDownloadsQueued: [], - showBookmarksModal: false + showBookmarksModal: false, + isDescriptionClamped: false, + showFullDescription: false } }, computed: { @@ -596,10 +601,15 @@ export default { this.$store.commit('setBookshelfBookIds', []) this.$store.commit('showEditModal', this.libraryItem) }, + checkDescriptionClamped() { + if (!this.$refs.description) return + this.isDescriptionClamped = this.$refs.description.scrollHeight > this.$refs.description.clientHeight + }, libraryItemUpdated(libraryItem) { if (libraryItem.id === this.libraryItemId) { console.log('Item was updated', libraryItem) this.libraryItem = libraryItem + this.$nextTick(this.checkDescriptionClamped) } }, clearProgressClick() { @@ -756,6 +766,8 @@ export default { } }, mounted() { + this.checkDescriptionClamped() + this.episodeDownloadsQueued = this.libraryItem.episodeDownloadsQueued || [] this.episodesDownloading = this.libraryItem.episodesDownloading || [] @@ -782,3 +794,18 @@ export default { } } </script> + +<style scoped> +#item-description { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + max-height: 6.25rem; + transition: all 0.3s ease-in-out; +} +#item-description.show-full { + -webkit-line-clamp: unset; + max-height: 999rem; +} +</style> \ No newline at end of file diff --git a/client/pages/library/_library/podcast/latest.vue b/client/pages/library/_library/podcast/latest.vue index 8d95203f..42f107c8 100644 --- a/client/pages/library/_library/podcast/latest.vue +++ b/client/pages/library/_library/podcast/latest.vue @@ -45,7 +45,7 @@ <widgets-podcast-type-indicator :type="episode.episodeType" /> </div> - <p class="text-sm text-gray-200 mb-4 episode-subtitle-long" v-html="episode.subtitle || episode.description" /> + <p class="text-sm text-gray-200 mb-4 line-clamp-4" v-html="episode.subtitle || episode.description" /> <div class="flex items-center"> <button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)"> From 58598bfcf283c24354f45c7a45a7857171193225 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 18 Feb 2024 11:32:24 -0600 Subject: [PATCH 0380/2145] Update:Clamp author description to 4 lines and add more button #2614 --- client/pages/author/_id.vue | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/client/pages/author/_id.vue b/client/pages/author/_id.vue index 5ef1bef2..4e72f061 100644 --- a/client/pages/author/_id.vue +++ b/client/pages/author/_id.vue @@ -17,7 +17,10 @@ </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 whitespace-pre-wrap">{{ author.description }}</p> + <p ref="description" id="author-description" class="text-white max-w-3xl text-base whitespace-pre-wrap" :class="{ 'show-full': showFullDescription }">{{ author.description }}</p> + <button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription"> + {{ showFullDescription ? 'Read less' : 'Read more' }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span> + </button> </div> </div> @@ -62,7 +65,10 @@ export default { } }, data() { - return {} + return { + isDescriptionClamped: false, + showFullDescription: false + } }, computed: { streamLibraryItem() { @@ -82,6 +88,10 @@ export default { } }, methods: { + checkDescriptionClamped() { + if (!this.$refs.description) return + this.isDescriptionClamped = this.$refs.description.scrollHeight > this.$refs.description.clientHeight + }, editAuthor() { this.$store.commit('globals/showEditAuthorModal', this.author) }, @@ -93,6 +103,7 @@ export default { series: this.authorSeries, libraryItems: this.libraryItems } + this.$nextTick(this.checkDescriptionClamped) } }, authorRemoved(author) { @@ -104,6 +115,7 @@ export default { }, mounted() { if (!this.author) this.$router.replace('/') + this.checkDescriptionClamped() this.$root.socket.on('author_updated', this.authorUpdated) this.$root.socket.on('author_removed', this.authorRemoved) @@ -113,4 +125,19 @@ export default { this.$root.socket.off('author_removed', this.authorRemoved) } } -</script> \ No newline at end of file +</script> + +<style scoped> +#author-description { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + max-height: 6.25rem; + transition: all 0.3s ease-in-out; +} +#author-description.show-full { + -webkit-line-clamp: unset; + max-height: 999rem; +} +</style> \ No newline at end of file From acf75abdf1921ae4a82381cc02732f422147c381 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 18 Feb 2024 13:06:51 -0600 Subject: [PATCH 0381/2145] Update:Match author use closest name match by levenshtein distance #2624 --- server/finders/AuthorFinder.js | 9 +++++- server/providers/Audnexus.js | 55 +++++++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/server/finders/AuthorFinder.js b/server/finders/AuthorFinder.js index 69aa724d..8dbe0097 100644 --- a/server/finders/AuthorFinder.js +++ b/server/finders/AuthorFinder.js @@ -15,12 +15,19 @@ class AuthorFinder { return this.audnexus.findAuthorByASIN(asin, region) } + /** + * + * @param {string} name + * @param {string} region + * @param {Object} [options={}] + * @returns {Promise<import('../providers/Audnexus').AuthorSearchObj>} + */ async findAuthorByName(name, region, options = {}) { if (!name) return null const maxLevenshtein = !isNaN(options.maxLevenshtein) ? Number(options.maxLevenshtein) : 3 const author = await this.audnexus.findAuthorByName(name, region, maxLevenshtein) - if (!author || !author.name) { + if (!author?.name) { return null } return author diff --git a/server/providers/Audnexus.js b/server/providers/Audnexus.js index b74d1d13..f5034bff 100644 --- a/server/providers/Audnexus.js +++ b/server/providers/Audnexus.js @@ -2,15 +2,30 @@ const axios = require('axios') const { levenshteinDistance } = require('../utils/index') const Logger = require('../Logger') +/** + * @typedef AuthorSearchObj + * @property {string} asin + * @property {string} description + * @property {string} image + * @property {string} name + */ + class Audnexus { constructor() { this.baseUrl = 'https://api.audnex.us' } + /** + * + * @param {string} name + * @param {string} region + * @returns {Promise<{asin:string, name:string}[]>} + */ authorASINsRequest(name, region) { - name = encodeURIComponent(name) - const regionQuery = region ? `®ion=${region}` : '' - const authorRequestUrl = `${this.baseUrl}/authors?name=${name}${regionQuery}` + const searchParams = new URLSearchParams() + searchParams.set('name', name) + if (region) searchParams.set('region', region) + const authorRequestUrl = `${this.baseUrl}/authors?${searchParams.toString()}` Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) return axios.get(authorRequestUrl).then((res) => { return res.data || [] @@ -20,6 +35,12 @@ class Audnexus { }) } + /** + * + * @param {string} asin + * @param {string} region + * @returns {Promise<AuthorSearchObj>} + */ authorRequest(asin, region) { asin = encodeURIComponent(asin) const regionQuery = region ? `?region=${region}` : '' @@ -33,6 +54,12 @@ class Audnexus { }) } + /** + * + * @param {string} asin + * @param {string} region + * @returns {Promise<AuthorSearchObj>} + */ async findAuthorByASIN(asin, region) { const author = await this.authorRequest(asin, region) if (!author) { @@ -46,14 +73,28 @@ class Audnexus { } } + /** + * + * @param {string} name + * @param {string} region + * @param {number} maxLevenshtein + * @returns {Promise<AuthorSearchObj>} + */ async findAuthorByName(name, region, maxLevenshtein = 3) { Logger.debug(`[Audnexus] Looking up author by name ${name}`) - const asins = await this.authorASINsRequest(name, region) - const matchingAsin = asins.find(obj => levenshteinDistance(obj.name, name) <= maxLevenshtein) - if (!matchingAsin) { + const authorAsinObjs = await this.authorASINsRequest(name, region) + + let closestMatch = null + authorAsinObjs.forEach((authorAsinObj) => { + authorAsinObj.levenshteinDistance = levenshteinDistance(authorAsinObj.name, name) + if (!closestMatch || closestMatch.levenshteinDistance > authorAsinObj.levenshteinDistance) { + closestMatch = authorAsinObj + } + }) + if (!closestMatch || closestMatch.levenshteinDistance > maxLevenshtein) { return null } - const author = await this.authorRequest(matchingAsin.asin) + const author = await this.authorRequest(closestMatch.asin) if (!author) { return null } From a43b93d7962fa3351db2fee4f7fa1f762259f977 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 18 Feb 2024 14:58:46 -0600 Subject: [PATCH 0382/2145] Fix:Clear library filter data cache when library item is updated #2597 --- server/Database.js | 13 ++++++++++++- server/models/LibraryItem.js | 6 ++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/server/Database.js b/server/Database.js index dd9a0550..3d4219e5 100644 --- a/server/Database.js +++ b/server/Database.js @@ -419,10 +419,21 @@ class Database { await this.models.libraryItem.fullCreateFromOld(oldLibraryItem) } + /** + * Save metadata file and update library item + * + * @param {import('./objects/LibraryItem')} oldLibraryItem + * @returns {Promise<boolean>} + */ async updateLibraryItem(oldLibraryItem) { if (!this.sequelize) return false await oldLibraryItem.saveMetadata() - return this.models.libraryItem.fullUpdateFromOld(oldLibraryItem) + const updated = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem) + // Clear library filter data cache + if (updated) { + delete this.libraryFilterData[oldLibraryItem.libraryId] + } + return updated } async removeLibraryItem(libraryItemId) { diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index ee8a4bb8..c7da31f6 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -225,6 +225,12 @@ class LibraryItem extends Model { return newLibraryItem } + /** + * Updates libraryItem, book, authors and series from old library item + * + * @param {oldLibraryItem} oldLibraryItem + * @returns {Promise<boolean>} true if updates were made + */ static async fullUpdateFromOld(oldLibraryItem) { const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, { include: [ From 973a18d3460637b2009feab89fdda1bfafe5e066 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 18 Feb 2024 15:38:45 -0600 Subject: [PATCH 0383/2145] Update:Added button to user edit modal for unlinking user from openid #2587 --- client/components/modals/AccountModal.vue | 36 +++++++++++++++++++++-- server/controllers/UserController.js | 17 +++++++++++ server/objects/user/User.js | 3 +- server/routers/ApiRouter.js | 2 +- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/client/components/modals/AccountModal.vue b/client/components/modals/AccountModal.vue index bdb8711d..ed8660a0 100644 --- a/client/components/modals/AccountModal.vue +++ b/client/components/modals/AccountModal.vue @@ -111,7 +111,8 @@ </div> <div class="flex pt-4 px-2"> - <ui-btn v-if="isEditingRoot" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn> + <ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">Unlink OpenID</ui-btn> + <ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn> <div class="flex-grow" /> <ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn> </div> @@ -136,7 +137,8 @@ export default { newUser: {}, isNew: true, tags: [], - loadingTags: false + loadingTags: false, + unlinkingFromOpenID: false } }, watch: { @@ -180,7 +182,7 @@ export default { return this.isNew ? this.$strings.HeaderNewAccount : this.$strings.HeaderUpdateAccount }, isEditingRoot() { - return this.account && this.account.type === 'root' + return this.account?.type === 'root' }, libraries() { return this.$store.state.libraries.libraries @@ -198,6 +200,9 @@ export default { }, tagsSelectionText() { return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser + }, + hasOpenIDLink() { + return !!this.account?.hasOpenIDLink } }, methods: { @@ -205,6 +210,31 @@ export default { // Force close when navigating - used in UsersTable if (this.$refs.modal) this.$refs.modal.setHide() }, + unlinkOpenID() { + const payload = { + message: 'Are you sure you want to unlink this user from OpenID?', + callback: (confirmed) => { + if (confirmed) { + this.unlinkingFromOpenID = true + this.$axios + .$patch(`/api/users/${this.account.id}/openid-unlink`) + .then(() => { + this.$toast.success('User unlinked from OpenID') + this.show = false + }) + .catch((error) => { + console.error('Failed to unlink user from OpenID', error) + this.$toast.error('Failed to unlink user from OpenID') + }) + .finally(() => { + this.unlinkingFromOpenID = false + }) + } + }, + type: 'yesNo' + } + this.$store.commit('globals/setConfirmPrompt', payload) + }, accessAllTagsToggled(val) { if (val) { if (this.newUser.itemTagsSelected?.length) { diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 86d2c78e..72677751 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -194,6 +194,23 @@ class UserController { }) } + /** + * PATCH: /api/users/:id/openid-unlink + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + async unlinkFromOpenID(req, res) { + Logger.debug(`[UserController] Unlinking user "${req.reqUser.username}" from OpenID with sub "${req.reqUser.authOpenIDSub}"`) + req.reqUser.authOpenIDSub = null + if (await Database.userModel.updateFromOld(req.reqUser)) { + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.reqUser.toJSONForBrowser()) + res.sendStatus(200) + } else { + res.sendStatus(500) + } + } + // GET: api/users/:id/listening-sessions async getListeningSessions(req, res) { var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id) diff --git a/server/objects/user/User.js b/server/objects/user/User.js index b503872d..d926e8be 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -117,7 +117,8 @@ class User { createdAt: this.createdAt, permissions: this.permissions, librariesAccessible: [...this.librariesAccessible], - itemTagsSelected: [...this.itemTagsSelected] + itemTagsSelected: [...this.itemTagsSelected], + hasOpenIDLink: !!this.authOpenIDSub } if (minimal) { delete json.mediaProgress diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 3deb4030..d18d93e6 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -130,7 +130,7 @@ class ApiRouter { this.router.get('/users/:id', UserController.middleware.bind(this), UserController.findOne.bind(this)) this.router.patch('/users/:id', UserController.middleware.bind(this), UserController.update.bind(this)) this.router.delete('/users/:id', UserController.middleware.bind(this), UserController.delete.bind(this)) - + this.router.patch('/users/:id/openid-unlink', UserController.middleware.bind(this), UserController.unlinkFromOpenID.bind(this)) this.router.get('/users/:id/listening-sessions', UserController.middleware.bind(this), UserController.getListeningSessions.bind(this)) this.router.get('/users/:id/listening-stats', UserController.middleware.bind(this), UserController.getListeningStats.bind(this)) From 335d39f31777bfbd17b5f31bd8d243625c4fb687 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 18 Feb 2024 16:22:10 -0600 Subject: [PATCH 0384/2145] Update guide link for custom metadata providers --- .../config/item-metadata-utils/custom-metadata-providers.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/config/item-metadata-utils/custom-metadata-providers.vue b/client/pages/config/item-metadata-utils/custom-metadata-providers.vue index 66581dae..dfaabca0 100644 --- a/client/pages/config/item-metadata-utils/custom-metadata-providers.vue +++ b/client/pages/config/item-metadata-utils/custom-metadata-providers.vue @@ -8,7 +8,7 @@ </template> <template #header-items> <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2"> - <a href="https://www.audiobookshelf.org/guides/#" target="_blank" class="inline-flex"> + <a href="https://www.audiobookshelf.org/guides/custom-metadata-providers" target="_blank" class="inline-flex"> <span class="material-icons text-xl w-5 text-gray-200">help_outline</span> </a> </ui-tooltip> From 85fecbd1b9fc424d8bfd1b63cbae45b8b23a7d34 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 18 Feb 2024 16:43:16 -0600 Subject: [PATCH 0385/2145] Version bump v2.8.0 --- client/package-lock.json | 6 +++--- client/package.json | 4 ++-- package-lock.json | 6 +++--- package.json | 2 +- server/objects/settings/ServerSettings.js | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index b13fe9ee..a15762dc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.7.2", + "version": "2.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.7.2", + "version": "2.8.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", @@ -16976,4 +16976,4 @@ } } } -} +} \ No newline at end of file diff --git a/client/package.json b/client/package.json index 6a852b03..d9f11c99 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.7.2", + "version": "2.8.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", @@ -36,4 +36,4 @@ "postcss": "^8.3.6", "tailwindcss": "^3.4.1" } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ac2d4cc6..a62598af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.7.2", + "version": "2.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.7.2", + "version": "2.8.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", @@ -5554,4 +5554,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index b44ef36e..88b1581f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.7.2", + "version": "2.8.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index d3a6bdf0..5cc68a5c 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -113,7 +113,7 @@ class ServerSettings { this.version = settings.version || null this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 - this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.7.3 + this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.8.0 this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null From e71a14756b73e20cec907aa2f0fef24dbfaac27a Mon Sep 17 00:00:00 2001 From: burghy86 <burghy@mail.com> Date: Mon, 19 Feb 2024 15:53:12 +0100 Subject: [PATCH 0386/2145] Update it.json new string update --- client/strings/it.json | 72 +++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/client/strings/it.json b/client/strings/it.json index 53394481..11865aad 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -32,8 +32,8 @@ "ButtonHide": "Nascondi", "ButtonHome": "Home", "ButtonIssues": "Errori", - "ButtonJumpBackward": "Jump Backward", - "ButtonJumpForward": "Jump Forward", + "ButtonJumpBackward": "Salta indietro", + "ButtonJumpForward": "Salta Avanti", "ButtonLatest": "Ultimi", "ButtonLibrary": "Libreria", "ButtonLogout": "Disconnetti", @@ -43,15 +43,15 @@ "ButtonMatchAllAuthors": "Aggiungi metadata agli Autori", "ButtonMatchBooks": "Aggiungi metadata della Libreria", "ButtonNevermind": "Nevermind", - "ButtonNextChapter": "Next Chapter", + "ButtonNextChapter": "Prossimo Capitolo", "ButtonOk": "Ok", "ButtonOpenFeed": "Apri Feed", "ButtonOpenManager": "Apri Manager", - "ButtonPause": "Pause", + "ButtonPause": "Pausa", "ButtonPlay": "Play", "ButtonPlaying": "In Riproduzione", "ButtonPlaylists": "Playlists", - "ButtonPreviousChapter": "Previous Chapter", + "ButtonPreviousChapter": "Capitolo Precendente", "ButtonPurgeAllCache": "Elimina tutta la Cache", "ButtonPurgeItemsCache": "Elimina la Cache selezionata", "ButtonPurgeMediaProgress": "Elimina info dei media ascoltati", @@ -92,15 +92,15 @@ "ButtonUserEdit": "Modifica Utente {0}", "ButtonViewAll": "Mostra Tutto", "ButtonYes": "Si", - "ErrorUploadFetchMetadataAPI": "Error fetching metadata", - "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", - "ErrorUploadLacksTitle": "Must have a title", + "ErrorUploadFetchMetadataAPI": "Errore Recupero metadati", + "ErrorUploadFetchMetadataNoResults": "Impossibile recuperare i metadati: prova a modificate il titolo e/o l'autore", + "ErrorUploadLacksTitle": "Deve avere un titolo", "HeaderAccount": "Account", "HeaderAdvanced": "Avanzate", "HeaderAppriseNotificationSettings": "Apprendi le impostazioni di Notifica", "HeaderAudiobookTools": "Utilità Audiobook File Management", "HeaderAudioTracks": "Tracce Audio", - "HeaderAuthentication": "Authentication", + "HeaderAuthentication": "Authenticazione", "HeaderBackups": "Backup", "HeaderChangePassword": "Cambia Password", "HeaderChapters": "Capitoli", @@ -111,7 +111,7 @@ "HeaderCurrentDownloads": "Download Correnti", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Dettagli", - "HeaderDownloadQueue": "Download Queue", + "HeaderDownloadQueue": "Download coda", "HeaderEbookFiles": "Ebook File", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", @@ -136,7 +136,7 @@ "HeaderManageTags": "Gestisci Tags", "HeaderMapDetails": "Mappa Dettagli", "HeaderMatch": "Trova Corrispondenza", - "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", + "HeaderMetadataOrderOfPrecedence": "Priorità ordine Metadata", "HeaderMetadataToEmbed": "Metadata da incorporare", "HeaderNewAccount": "Nuovo Account", "HeaderNewLibrary": "Nuova Libreria", @@ -205,12 +205,12 @@ "LabelAuthorLastFirst": "Autori (Per Cognome)", "LabelAuthors": "Autori", "LabelAutoDownloadEpisodes": "Auto Download Episodi", - "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": "Auto controllo Metadata", + "LabelAutoFetchMetadataHelp": "Recupera i metadati per titolo, autore e serie per semplificare il caricamento. Potrebbe essere necessario abbinare metadati aggiuntivi dopo il caricamento.", "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", + "LabelAutoLaunchDescription": "Reindirizzamento automatico al provider di autenticazione quando si accede alla pagina di accesso (percorso di sostituzione manuale <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Auto Registrazione", + "LabelAutoRegisterDescription": "Crea automaticamente nuovi utenti dopo aver effettuato l'accesso", "LabelBackToUser": "Torna a Utenti", "LabelBackupLocation": "Percorso del Backup", "LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico", @@ -221,7 +221,7 @@ "LabelBackupsNumberToKeepHelp": "Verrà rimosso solo 1 backup alla volta, quindi se hai più backup, dovrai rimuoverli manualmente.", "LabelBitrate": "Bitrate", "LabelBooks": "Libri", - "LabelButtonText": "Button Text", + "LabelButtonText": "Buttone Testo", "LabelChangePassword": "Cambia Password", "LabelChannels": "Canali", "LabelChapters": "Capitoli", @@ -277,7 +277,7 @@ "LabelExample": "Esempio", "LabelExplicit": "Esplicito", "LabelFeedURL": "Feed URL", - "LabelFetchingMetadata": "Fetching Metadata", + "LabelFetchingMetadata": "Recupero dei metadati", "LabelFile": "File", "LabelFileBirthtime": "Data Creazione", "LabelFileModified": "Ultima modifica", @@ -298,7 +298,7 @@ "LabelHardDeleteFile": "Elimina Definitivamente", "LabelHasEbook": "Un ebook", "LabelHasSupplementaryEbook": "Un ebook Supplementare", - "LabelHighestPriority": "Highest priority", + "LabelHighestPriority": "Priorità Massima", "LabelHost": "Host", "LabelHour": "Ora", "LabelIcon": "Icona", @@ -340,20 +340,20 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Allarme", "LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data", - "LabelLowestPriority": "Lowest Priority", - "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", + "LabelLowestPriority": "Priorità Minima", + "LabelMatchExistingUsersBy": "Abbina gli utenti esistenti per", + "LabelMatchExistingUsersByDescription": "Utilizzato per connettere gli utenti esistenti. Una volta connessi, gli utenti verranno abbinati a un ID univoco dal tuo provider SSO", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Tipo Media", - "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", + "LabelMetadataOrderOfPrecedenceDescription": "Le origini di metadati con priorità più alta sovrascriveranno le origini di metadati con priorità inferiore", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", "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.", + "LabelMissingParts": "Parti rimanenti", + "LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti", + "LabelMobileRedirectURIsDescription": "Questa è una lista bianca di URI di reindirizzamento validi per le app mobili. Quello predefinito è <code>audiobookshelf://oauth</code>, che puoi rimuovere o integrare con URI aggiuntivi per l'integrazione di app di terze parti. Utilizzando un asterisco (<code>*</code>) poiché l'unica voce consente qualsiasi URI.", "LabelMore": "Molto", "LabelMoreInfo": "Più Info", "LabelName": "Nome", @@ -413,7 +413,7 @@ "LabelRecentlyAdded": "Aggiunti Recentemente", "LabelRecentSeries": "Serie Recenti", "LabelRecommended": "Raccomandati", - "LabelRedo": "Redo", + "LabelRedo": "Rifai", "LabelRegion": "Regione", "LabelReleaseDate": "Data Release", "LabelRemoveCover": "Rimuovi cover", @@ -475,7 +475,7 @@ "LabelShowAll": "Mostra Tutto", "LabelSize": "Dimensione", "LabelSleepTimer": "Sleep timer", - "LabelSlug": "Slug", + "LabelSlug": "Lento", "LabelStart": "Inizo", "LabelStarted": "Iniziato", "LabelStartedAt": "Iniziato al", @@ -502,9 +502,9 @@ "LabelTagsAccessibleToUser": "Tags permessi agli Utenti", "LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti", "LabelTasks": "Processi in esecuzione", - "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorBulletedList": "Elenco puntato", "LabelTextEditorLink": "Link", - "LabelTextEditorNumberedList": "Numbered list", + "LabelTextEditorNumberedList": "Elenco Numerato", "LabelTextEditorUnlink": "Unlink", "LabelTheme": "Tema", "LabelThemeDark": "Scuro", @@ -527,7 +527,7 @@ "LabelTrackFromMetadata": "Traccia da Metadata", "LabelTracks": "Traccia", "LabelTracksMultiTrack": "Multi-traccia", - "LabelTracksNone": "No tracks", + "LabelTracksNone": "Nessuna traccia", "LabelTracksSingleTrack": "Traccia-singola", "LabelType": "Tipo", "LabelUnabridged": "Integrale", @@ -540,7 +540,7 @@ "LabelUpdateDetailsHelp": "Consenti la sovrascrittura dei dettagli esistenti per i libri selezionati quando viene individuata una corrispondenza", "LabelUploaderDragAndDrop": "Drag & drop file o Cartelle", "LabelUploaderDropFiles": "Elimina file", - "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", + "LabelUploaderItemFetchMetadataHelp": "Recupera automaticamente titolo, autore e serie", "LabelUseChapterTrack": "Usa il Capitolo della Traccia", "LabelUseFullTrack": "Usa la traccia totale", "LabelUser": "Utente", @@ -588,7 +588,7 @@ "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?", - "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", + "MessageConfirmRemoveListeningSessions": "Sei sicuro di voler rimuovere {0} sessioni di Ascolto?", "MessageConfirmRemoveNarrator": "Sei sicuro di voler rimuovere il narratore \"{0}\"?", "MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?", "MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?", @@ -668,7 +668,7 @@ "MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su", "MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.", "MessageSearchResultsFor": "cerca risultati per", - "MessageSelected": "{0} selected", + "MessageSelected": "{0} selezionati", "MessageServerCouldNotBeReached": "Impossibile raggiungere il server", "MessageSetChaptersFromTracksDescription": "Impostare i capitoli utilizzando ciascun file audio come capitolo e il titolo del capitolo come nome del file audio", "MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?", @@ -757,7 +757,7 @@ "ToastSendEbookToDeviceFailed": "Impossibile inviare l'ebook al dispositivo", "ToastSendEbookToDeviceSuccess": "Ebook inviato al dispositivo \"{0}\"", "ToastSeriesUpdateFailed": "Aggiornamento Serie Fallito", - "ToastSeriesUpdateSuccess": "Serie Aggornate", + "ToastSeriesUpdateSuccess": "Serie Aggiornate", "ToastSessionDeleteFailed": "Errore eliminazione sessione", "ToastSessionDeleteSuccess": "Sessione cancellata", "ToastSocketConnected": "Socket connesso", @@ -765,4 +765,4 @@ "ToastSocketFailedToConnect": "Socket non riesce a connettersi", "ToastUserDeleteFailed": "Errore eliminazione utente", "ToastUserDeleteSuccess": "Utente eliminato" -} \ No newline at end of file +} From 60ea386c6db44ec5e6f30c1450f0a87327b63773 Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Tue, 20 Feb 2024 05:57:52 -0300 Subject: [PATCH 0387/2145] Updated pt-br string --- client/strings/pt-br.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index cd4e9cab..5bb91aed 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -396,7 +396,7 @@ "LabelPlayMethod": "Método de Reprodução", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", - "LabelPodcastSearchRegion": "Podcast search region", + "LabelPodcastSearchRegion": "Região para pesquisa de podcast", "LabelPodcastType": "Tipo de Podcast", "LabelPort": "Porta", "LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)", @@ -765,4 +765,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} \ No newline at end of file +} From 2c589c1dbdca3b8926b6242fd0da8b50859a1677 Mon Sep 17 00:00:00 2001 From: lonezel <127624553+lonezel@users.noreply.github.com> Date: Tue, 20 Feb 2024 10:19:48 +0100 Subject: [PATCH 0388/2145] Update de.json Translated new strings to german - this is my first ever commit on Github, if I need to change something in my workflow let me know! --- 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 ff8e991a..5c43ef17 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -32,8 +32,8 @@ "ButtonHide": "Ausblenden", "ButtonHome": "Startseite", "ButtonIssues": "Probleme", - "ButtonJumpBackward": "Jump Backward", - "ButtonJumpForward": "Jump Forward", + "ButtonJumpBackward": "Zurück springen", + "ButtonJumpForward": "Vorwärts springen", "ButtonLatest": "Neuste", "ButtonLibrary": "Bibliothek", "ButtonLogout": "Abmelden", @@ -43,7 +43,7 @@ "ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)", "ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)", "ButtonNevermind": "Abbrechen", - "ButtonNextChapter": "Next Chapter", + "ButtonNextChapter": "Nächstes Kapitel", "ButtonOk": "Ok", "ButtonOpenFeed": "Feed öffnen", "ButtonOpenManager": "Manager öffnen", @@ -51,7 +51,7 @@ "ButtonPlay": "Abspielen", "ButtonPlaying": "Spielt", "ButtonPlaylists": "Wiedergabelisten", - "ButtonPreviousChapter": "Previous Chapter", + "ButtonPreviousChapter": "Vorheriges Kapitel", "ButtonPurgeAllCache": "Cache leeren", "ButtonPurgeItemsCache": "Lösche Medien-Cache", "ButtonPurgeMediaProgress": "Lösche Hörfortschritte", @@ -109,7 +109,7 @@ "HeaderCollectionItems": "Sammlungseinträge", "HeaderCover": "Titelbild", "HeaderCurrentDownloads": "Aktuelle Downloads", - "HeaderCustomMetadataProviders": "Custom Metadata Providers", + "HeaderCustomMetadataProviders": "Benutzerdefinierte Metadata Anbieter", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Warteschlange", "HeaderEbookFiles": "E-Book Dateien", @@ -502,10 +502,10 @@ "LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter", "LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter", "LabelTasks": "Laufende Aufgaben", - "LabelTextEditorBulletedList": "Bulleted list", + "LabelTextEditorBulletedList": "Aufzählungsliste", "LabelTextEditorLink": "Link", - "LabelTextEditorNumberedList": "Numbered list", - "LabelTextEditorUnlink": "Unlink", + "LabelTextEditorNumberedList": "nummerierte Liste", + "LabelTextEditorUnlink": "entkoppeln", "LabelTheme": "Theme", "LabelThemeDark": "Dunkel", "LabelThemeLight": "Hell", @@ -531,7 +531,7 @@ "LabelTracksSingleTrack": "Einzeldatei", "LabelType": "Typ", "LabelUnabridged": "Ungekürzt", - "LabelUndo": "Undo", + "LabelUndo": "Rückgängig machen", "LabelUnknown": "Unbekannt", "LabelUpdateCover": "Titelbild aktualisieren", "LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird", @@ -765,4 +765,4 @@ "ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen", "ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden", "ToastUserDeleteSuccess": "Benutzer gelöscht" -} \ No newline at end of file +} From a4dcb4f92ec42a13cd3236f3c203b44c4affdf24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hunyady=20Mih=C3=A1ly?= <mihaly.hunyady@emarsys.com> Date: Tue, 20 Feb 2024 12:25:43 +0100 Subject: [PATCH 0389/2145] feat(i18n): add Hungarian translation --- client/plugins/i18n.js | 1 + client/strings/hu.json | 768 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 769 insertions(+) create mode 100644 client/strings/hu.json diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index d7fc972e..a4a328c7 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -14,6 +14,7 @@ const languageCodeMap = { 'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' }, 'it': { label: 'Italiano', dateFnsLocale: 'it' }, 'lt': { label: 'Lietuvių', dateFnsLocale: 'lt' }, + 'hu': { label: 'Magyar', dateFnsLocale: 'hu' }, 'nl': { label: 'Nederlands', dateFnsLocale: 'nl' }, 'no': { label: 'Norsk', dateFnsLocale: 'no' }, 'pl': { label: 'Polski', dateFnsLocale: 'pl' }, diff --git a/client/strings/hu.json b/client/strings/hu.json new file mode 100644 index 00000000..9d79f28e --- /dev/null +++ b/client/strings/hu.json @@ -0,0 +1,768 @@ +{ + "ButtonAdd": "Hozzáadás", + "ButtonAddChapters": "Fejezetek hozzáadása", + "ButtonAddDevice": "Eszköz hozzáadása", + "ButtonAddLibrary": "Könyvtár hozzáadása", + "ButtonAddPodcasts": "Podcastok hozzáadása", + "ButtonAddUser": "Felhasználó hozzáadása", + "ButtonAddYourFirstLibrary": "Az első könyvtár hozzáadása", + "ButtonApply": "Alkalmaz", + "ButtonApplyChapters": "Fejezetek alkalmazása", + "ButtonAuthors": "Szerzők", + "ButtonBrowseForFolder": "Mappa keresése", + "ButtonCancel": "Mégse", + "ButtonCancelEncode": "Kódolás megszakítása", + "ButtonChangeRootPassword": "Gyökérjelszó megváltoztatása", + "ButtonCheckAndDownloadNewEpisodes": "Új epizódok ellenőrzése és letöltése", + "ButtonChooseAFolder": "Válassz egy mappát", + "ButtonChooseFiles": "Fájlok kiválasztása", + "ButtonClearFilter": "Szűrő törlése", + "ButtonCloseFeed": "Hírcsatorna bezárása", + "ButtonCollections": "Gyűjtemény", + "ButtonConfigureScanner": "Szkenner konfigurálása", + "ButtonCreate": "Létrehozás", + "ButtonCreateBackup": "Biztonsági másolat készítése", + "ButtonDelete": "Törlés", + "ButtonDownloadQueue": "Sor", + "ButtonEdit": "Szerkesztés", + "ButtonEditChapters": "Fejezetek szerkesztése", + "ButtonEditPodcast": "Podcast szerkesztése", + "ButtonForceReScan": "Újraszkennelés kényszerítése", + "ButtonFullPath": "Teljes útvonal", + "ButtonHide": "Elrejtés", + "ButtonHome": "Kezdőlap", + "ButtonIssues": "Problémák", + "ButtonJumpBackward": "Ugrás vissza", + "ButtonJumpForward": "Ugrás előre", + "ButtonLatest": "Legújabb", + "ButtonLibrary": "Könyvtár", + "ButtonLogout": "Kijelentkezés", + "ButtonLookup": "Keresés", + "ButtonManageTracks": "Sávok kezelése", + "ButtonMapChapterTitles": "Fejezetcímek hozzárendelése", + "ButtonMatchAllAuthors": "Minden szerző egyeztetése", + "ButtonMatchBooks": "Könyvek egyeztetése", + "ButtonNevermind": "Mindegy", + "ButtonNextChapter": "Következő fejezet", + "ButtonOk": "Oké", + "ButtonOpenFeed": "Hírcsatorna megnyitása", + "ButtonOpenManager": "Kezelő megnyitása", + "ButtonPause": "Szünet", + "ButtonPlay": "Lejátszás", + "ButtonPlaying": "Lejátszás folyamatban", + "ButtonPlaylists": "Lejátszási listák", + "ButtonPreviousChapter": "Előző fejezet", + "ButtonPurgeAllCache": "Összes gyorsítótár törlése", + "ButtonPurgeItemsCache": "Elemek gyorsítótárának törlése", + "ButtonPurgeMediaProgress": "Médialejátszás állapotának törlése", + "ButtonQueueAddItem": "Hozzáadás a sorhoz", + "ButtonQueueRemoveItem": "Eltávolítás a sorból", + "ButtonQuickMatch": "Gyors egyeztetés", + "ButtonRead": "Olvasás", + "ButtonRemove": "Eltávolítás", + "ButtonRemoveAll": "Összes eltávolítása", + "ButtonRemoveAllLibraryItems": "Összes könyvtárelem eltávolítása", + "ButtonRemoveFromContinueListening": "Eltávolítás a Folytatás hallgatásából", + "ButtonRemoveFromContinueReading": "Eltávolítás a Folytatás olvasásából", + "ButtonRemoveSeriesFromContinueSeries": "Sorozat eltávolítása a Folytatás sorozatokból", + "ButtonReScan": "Újraszkennelés", + "ButtonReset": "Visszaállítás", + "ButtonResetToDefault": "Alapértelmezésre állítás", + "ButtonRestore": "Visszaállítás", + "ButtonSave": "Mentés", + "ButtonSaveAndClose": "Mentés és bezárás", + "ButtonSaveTracklist": "Sávlista mentése", + "ButtonScan": "Szkennelés", + "ButtonScanLibrary": "Könyvtár szkennelése", + "ButtonSearch": "Keresés", + "ButtonSelectFolderPath": "Mappa útvonalának kiválasztása", + "ButtonSeries": "Sorozatok", + "ButtonSetChaptersFromTracks": "Fejezetek beállítása sávokból", + "ButtonShiftTimes": "Idők eltolása", + "ButtonShow": "Megjelenítés", + "ButtonStartM4BEncode": "M4B kódolás indítása", + "ButtonStartMetadataEmbed": "Metaadatok beágyazásának indítása", + "ButtonSubmit": "Beküldés", + "ButtonTest": "Teszt", + "ButtonUpload": "Feltöltés", + "ButtonUploadBackup": "Biztonsági másolat feltöltése", + "ButtonUploadCover": "Borító feltöltése", + "ButtonUploadOPMLFile": "OPML fájl feltöltése", + "ButtonUserDelete": "Felhasználó törlése {0}", + "ButtonUserEdit": "Felhasználó szerkesztése {0}", + "ButtonViewAll": "Összes megtekintése", + "ButtonYes": "Igen", + "ErrorUploadFetchMetadataAPI": "Hiba a metaadatok lekérésekor", + "ErrorUploadFetchMetadataNoResults": "Nem sikerült a metaadatok lekérése - próbálja meg frissíteni a címet és/vagy a szerzőt", + "ErrorUploadLacksTitle": "Cím szükséges", + "HeaderAccount": "Fiók", + "HeaderAdvanced": "Haladó", + "HeaderAppriseNotificationSettings": "Apprise értesítési beállítások", + "HeaderAudiobookTools": "Hangoskönyv fájlkezelő eszközök", + "HeaderAudioTracks": "Audiósávok", + "HeaderAuthentication": "Hitelesítés", + "HeaderBackups": "Biztonsági másolatok", + "HeaderChangePassword": "Jelszó megváltoztatása", + "HeaderChapters": "Fejezetek", + "HeaderChooseAFolder": "Válasszon egy mappát", + "HeaderCollection": "Gyűjtemény", + "HeaderCollectionItems": "Gyűjtemény elemek", + "HeaderCover": "Borító", + "HeaderCurrentDownloads": "Jelenlegi letöltések", + "HeaderCustomMetadataProviders": "Egyéni metaadat-szolgáltatók", + "HeaderDetails": "Részletek", + "HeaderDownloadQueue": "Letöltési sor", + "HeaderEbookFiles": "E-könyv fájlok", + "HeaderEmail": "E-mail", + "HeaderEmailSettings": "E-mail beállítások", + "HeaderEpisodes": "Epizódok", + "HeaderEreaderDevices": "E-olvasó eszközök", + "HeaderEreaderSettings": "E-olvasó beállítások", + "HeaderFiles": "Fájlok", + "HeaderFindChapters": "Fejezetek keresése", + "HeaderIgnoredFiles": "Figyelmen kívül hagyott fájlok", + "HeaderItemFiles": "Elemfájlok", + "HeaderItemMetadataUtils": "Elem metaadat eszközök", + "HeaderLastListeningSession": "Utolsó hallgatási munkamenet", + "HeaderLatestEpisodes": "Legújabb epizódok", + "HeaderLibraries": "Könyvtárak", + "HeaderLibraryFiles": "Könyvtárfájlok", + "HeaderLibraryStats": "Könyvtár statisztikák", + "HeaderListeningSessions": "Hallgatási munkamenetek", + "HeaderListeningStats": "Hallgatási statisztikák", + "HeaderLogin": "Bejelentkezés", + "HeaderLogs": "Naplók", + "HeaderManageGenres": "Műfajok kezelése", + "HeaderManageTags": "Címkék kezelése", + "HeaderMapDetails": "Részletek hozzárendelése", + "HeaderMatch": "Egyeztetés", + "HeaderMetadataOrderOfPrecedence": "Metaadatok előnyben részesítési sorrendje", + "HeaderMetadataToEmbed": "Beágyazandó metaadatok", + "HeaderNewAccount": "Új fiók", + "HeaderNewLibrary": "Új könyvtár", + "HeaderNotifications": "Értesítések", + "HeaderOpenIDConnectAuthentication": "OpenID Connect hitelesítés", + "HeaderOpenRSSFeed": "RSS hírcsatorna megnyitása", + "HeaderOtherFiles": "Egyéb fájlok", + "HeaderPasswordAuthentication": "Jelszó hitelesítés", + "HeaderPermissions": "Engedélyek", + "HeaderPlayerQueue": "Lejátszó sor", + "HeaderPlaylist": "Lejátszási lista", + "HeaderPlaylistItems": "Lejátszási lista elemek", + "HeaderPodcastsToAdd": "Hozzáadandó podcastok", + "HeaderPreviewCover": "Borító előnézete", + "HeaderRemoveEpisode": "Epizód eltávolítása", + "HeaderRemoveEpisodes": "{0} epizód eltávolítása", + "HeaderRSSFeedGeneral": "RSS részletek", + "HeaderRSSFeedIsOpen": "RSS hírcsatorna nyitva", + "HeaderRSSFeeds": "RSS hírcsatornák", + "HeaderSavedMediaProgress": "Mentett médialejátszási állapot", + "HeaderSchedule": "Ütemezés", + "HeaderScheduleLibraryScans": "Könyvtárak automatikus szkennelésének ütemezése", + "HeaderSession": "Munkamenet", + "HeaderSetBackupSchedule": "Biztonsági másolatok ütemezésének beállítása", + "HeaderSettings": "Beállítások", + "HeaderSettingsDisplay": "Kijelző", + "HeaderSettingsExperimental": "Kísérleti funkciók", + "HeaderSettingsGeneral": "Általános", + "HeaderSettingsScanner": "Szkenner", + "HeaderSleepTimer": "Alvásidőzítő", + "HeaderStatsLargestItems": "Legnagyobb elemek", + "HeaderStatsLongestItems": "Leghosszabb elemek (órákban)", + "HeaderStatsMinutesListeningChart": "Hallgatási percek (az utolsó 7 napban)", + "HeaderStatsRecentSessions": "Legutóbbi munkamenetek", + "HeaderStatsTop10Authors": "Top 10 szerzők", + "HeaderStatsTop5Genres": "Top 5 műfajok", + "HeaderTableOfContents": "Tartalomjegyzék", + "HeaderTools": "Eszközök", + "HeaderUpdateAccount": "Fiók frissítése", + "HeaderUpdateAuthor": "Szerző frissítése", + "HeaderUpdateDetails": "Részletek frissítése", + "HeaderUpdateLibrary": "Könyvtár frissítése", + "HeaderUsers": "Felhasználók", + "HeaderYourStats": "Saját statisztikák", + "LabelAbridged": "Tömörített", + "LabelAccountType": "Fióktípus", + "LabelAccountTypeAdmin": "Admin", + "LabelAccountTypeGuest": "Vendég", + "LabelAccountTypeUser": "Felhasználó", + "LabelActivity": "Tevékenység", + "LabelAdded": "Hozzáadva", + "LabelAddedAt": "Hozzáadás ideje", + "LabelAddToCollection": "Hozzáadás a gyűjteményhez", + "LabelAddToCollectionBatch": "{0} könyv hozzáadása a gyűjteményhez", + "LabelAddToPlaylist": "Hozzáadás a lejátszási listához", + "LabelAddToPlaylistBatch": "{0} elem hozzáadása a lejátszási listához", + "LabelAdminUsersOnly": "Csak admin felhasználók", + "LabelAll": "Minden", + "LabelAllUsers": "Minden felhasználó", + "LabelAllUsersExcludingGuests": "Minden felhasználó, vendégek kivételével", + "LabelAllUsersIncludingGuests": "Minden felhasználó, beleértve a vendégeket is", + "LabelAlreadyInYourLibrary": "Már a könyvtárában van", + "LabelAppend": "Hozzáfűzés", + "LabelAuthor": "Szerző", + "LabelAuthorFirstLast": "Szerző (Keresztnév Vezetéknév)", + "LabelAuthorLastFirst": "Szerző (Vezetéknév, Keresztnév)", + "LabelAuthors": "Szerzők", + "LabelAutoDownloadEpisodes": "Epizódok automatikus letöltése", + "LabelAutoFetchMetadata": "Metaadatok automatikus lekérése", + "LabelAutoFetchMetadataHelp": "Cím, szerző és sorozat metaadatok automatikus lekérése a feltöltés megkönnyítése érdekében. További metaadatok egyeztetése szükséges lehet a feltöltés után.", + "LabelAutoLaunch": "Automatikus indítás", + "LabelAutoLaunchDescription": "Automatikus átirányítás az hitelesítő szolgáltatóhoz a bejelentkezési oldalra navigáláskor (kézi felülbírálás útvonala <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Automatikus regisztráció", + "LabelAutoRegisterDescription": "Új felhasználók automatikus létrehozása bejelentkezés után", + "LabelBackToUser": "Vissza a felhasználóhoz", + "LabelBackupLocation": "Biztonsági másolat helye", + "LabelBackupsEnableAutomaticBackups": "Automatikus biztonsági másolatok engedélyezése", + "LabelBackupsEnableAutomaticBackupsHelp": "Biztonsági másolatok mentése a /metadata/backups mappába", + "LabelBackupsMaxBackupSize": "Maximális biztonsági másolat méret (GB-ban)", + "LabelBackupsMaxBackupSizeHelp": "A rossz konfiguráció elleni védelem érdekében a biztonsági másolatok meghiúsulnak, ha meghaladják a beállított méretet.", + "LabelBackupsNumberToKeep": "Megtartandó biztonsági másolatok száma", + "LabelBackupsNumberToKeepHelp": "Egyszerre csak 1 biztonsági másolat kerül eltávolításra, tehát ha már több biztonsági másolat van, mint ez a szám, akkor manuálisan kell eltávolítani őket.", + "LabelBitrate": "Bitráta", + "LabelBooks": "Könyvek", + "LabelButtonText": "Gomb szövege", + "LabelChangePassword": "Jelszó megváltoztatása", + "LabelChannels": "Csatornák", + "LabelChapters": "Fejezetek", + "LabelChaptersFound": "fejezet található", + "LabelChapterTitle": "Fejezet címe", + "LabelClickForMoreInfo": "További információkért kattintson", + "LabelClosePlayer": "Lejátszó bezárása", + "LabelCodec": "Kodek", + "LabelCollapseSeries": "Sorozat összecsukása", + "LabelCollection": "Gyűjtemény", + "LabelCollections": "Gyűjtemények", + "LabelComplete": "Teljes", + "LabelConfirmPassword": "Jelszó megerősítése", + "LabelContinueListening": "Hallgatás folytatása", + "LabelContinueReading": "Olvasás folytatása", + "LabelContinueSeries": "Sorozat folytatása", + "LabelCover": "Borító", + "LabelCoverImageURL": "Borítókép URL", + "LabelCreatedAt": "Létrehozás ideje", + "LabelCronExpression": "Cron kifejezés", + "LabelCurrent": "Jelenlegi", + "LabelCurrently": "Jelenleg:", + "LabelCustomCronExpression": "Egyéni Cron kifejezés:", + "LabelDatetime": "Dátumidő", + "LabelDeleteFromFileSystemCheckbox": "Törlés a fájlrendszerről (ne jelölje be, ha csak az adatbázisból szeretné eltávolítani)", + "LabelDescription": "Leírás", + "LabelDeselectAll": "Minden kijelölés megszüntetése", + "LabelDevice": "Eszköz", + "LabelDeviceInfo": "Eszköz információ", + "LabelDeviceIsAvailableTo": "Eszköz elérhető a következő számára...", + "LabelDirectory": "Könyvtár", + "LabelDiscFromFilename": "Lemez a fájlnévből", + "LabelDiscFromMetadata": "Lemez a metaadatokból", + "LabelDiscover": "Felfedezés", + "LabelDownload": "Letöltés", + "LabelDownloadNEpisodes": "{0} epizód letöltése", + "LabelDuration": "Időtartam", + "LabelDurationFound": "Megtalált időtartam:", + "LabelEbook": "E-könyv", + "LabelEbooks": "E-könyvek", + "LabelEdit": "Szerkesztés", + "LabelEmail": "E-mail", + "LabelEmailSettingsFromAddress": "Feladó címe", + "LabelEmailSettingsSecure": "Biztonságos", + "LabelEmailSettingsSecureHelp": "Ha igaz, a kapcsolat TLS-t használ a szerverhez való csatlakozáskor. Ha hamis, akkor TLS-t használ, ha a szerver támogatja a STARTTLS kiterjesztést. A legtöbb esetben állítsa ezt az értéket igazra, ha a 465-ös portra csatlakozik. A 587-es vagy 25-ös port esetében tartsa hamis értéken. (a nodemailer.com/smtp/#authentication oldalról)", + "LabelEmailSettingsTestAddress": "Teszt cím", + "LabelEmbeddedCover": "Beágyazott borító", + "LabelEnable": "Engedélyezés", + "LabelEnd": "Vége", + "LabelEpisode": "Epizód", + "LabelEpisodeTitle": "Epizód címe", + "LabelEpisodeType": "Epizód típusa", + "LabelExample": "Példa", + "LabelExplicit": "Explicit", + "LabelFeedURL": "Hírcsatorna URL", + "LabelFetchingMetadata": "Metaadatok lekérése", + "LabelFile": "Fájl", + "LabelFileBirthtime": "Fájl létrehozásának ideje", + "LabelFileModified": "Fájl módosításának ideje", + "LabelFilename": "Fájlnév", + "LabelFilterByUser": "Szűrés felhasználó szerint", + "LabelFindEpisodes": "Epizódok keresése", + "LabelFinished": "Befejezett", + "LabelFolder": "Mappa", + "LabelFolders": "Mappák", + "LabelFontBold": "Félkövér", + "LabelFontFamily": "Betűtípus család", + "LabelFontItalic": "Dőlt", + "LabelFontScale": "Betűméret skála", + "LabelFontStrikethrough": "Áthúzott", + "LabelFormat": "Formátum", + "LabelGenre": "Műfaj", + "LabelGenres": "Műfajok", + "LabelHardDeleteFile": "Fájl végleges törlése", + "LabelHasEbook": "Van e-könyve", + "LabelHasSupplementaryEbook": "Van kiegészítő e-könyve", + "LabelHighestPriority": "Legmagasabb prioritás", + "LabelHost": "Hoszt", + "LabelHour": "Óra", + "LabelIcon": "Ikon", + "LabelImageURLFromTheWeb": "Kép URL a weben", + "LabelIncludeInTracklist": "Beleértve a sávlistába", + "LabelIncomplete": "Befejezetlen", + "LabelInProgress": "Folyamatban", + "LabelInterval": "Intervallum", + "LabelIntervalCustomDailyWeekly": "Egyéni napi/heti", + "LabelIntervalEvery12Hours": "Minden 12 órában", + "LabelIntervalEvery15Minutes": "Minden 15 percben", + "LabelIntervalEvery2Hours": "Minden 2 órában", + "LabelIntervalEvery30Minutes": "Minden 30 percben", + "LabelIntervalEvery6Hours": "Minden 6 órában", + "LabelIntervalEveryDay": "Minden nap", + "LabelIntervalEveryHour": "Minden órában", + "LabelInvalidParts": "Érvénytelen részek", + "LabelInvert": "Megfordítás", + "LabelItem": "Elem", + "LabelLanguage": "Nyelv", + "LabelLanguageDefaultServer": "Szerver alapértelmezett nyelve", + "LabelLastBookAdded": "Utolsó hozzáadott könyv", + "LabelLastBookUpdated": "Utolsó frissített könyv", + "LabelLastSeen": "Utolsó látogatás", + "LabelLastTime": "Utolsó alkalom", + "LabelLastUpdate": "Utolsó frissítés", + "LabelLayout": "Elrendezés", + "LabelLayoutSinglePage": "Egyoldalas", + "LabelLayoutSplitPage": "Kétoldalas", + "LabelLess": "Kevesebb", + "LabelLibrariesAccessibleToUser": "A felhasználó számára elérhető könyvtárak", + "LabelLibrary": "Könyvtár", + "LabelLibraryItem": "Könyvtári elem", + "LabelLibraryName": "Könyvtár neve", + "LabelLimit": "Korlát", + "LabelLineSpacing": "Sorköz", + "LabelListenAgain": "Újrahallgatás", + "LabelLogLevelDebug": "Debug", + "LabelLogLevelInfo": "Információ", + "LabelLogLevelWarn": "Figyelmeztetés", + "LabelLookForNewEpisodesAfterDate": "Új epizódok keresése ezen a dátum után", + "LabelLowestPriority": "Legalacsonyabb prioritás", + "LabelMatchExistingUsersBy": "Meglévő felhasználók egyeztetése", + "LabelMatchExistingUsersByDescription": "Meglévő felhasználók összekapcsolására használt. Egyszer összekapcsolva, a felhasználók egyedülálló azonosítóval lesznek egyeztetve az Ön SSO szolgáltatójától", + "LabelMediaPlayer": "Médialejátszó", + "LabelMediaType": "Média típus", + "LabelMetadataOrderOfPrecedenceDescription": "A magasabb prioritású metaadat-források felülírják az alacsonyabb prioritásúakat", + "LabelMetadataProvider": "Metaadat-szolgáltató", + "LabelMetaTag": "Meta címke", + "LabelMetaTags": "Meta címkék", + "LabelMinute": "Perc", + "LabelMissing": "Hiányzó", + "LabelMissingParts": "Hiányzó részek", + "LabelMobileRedirectURIs": "Engedélyezett mobil átirányítási URI-k", + "LabelMobileRedirectURIsDescription": "Ez egy fehérlista az érvényes mobilalkalmazás-átirányítási URI-k számára. Az alapértelmezett <code>audiobookshelf://oauth</code>, amely eltávolítható vagy kiegészíthető további URI-kkal harmadik féltől származó alkalmazásintegráció érdekében. Ha az egyetlen bejegyzés egy csillag (<code>*</code>), akkor bármely URI engedélyezett.", + "LabelMore": "Több", + "LabelMoreInfo": "További információ", + "LabelName": "Név", + "LabelNarrator": "Előadó", + "LabelNarrators": "Előadók", + "LabelNew": "Új", + "LabelNewestAuthors": "Legújabb szerzők", + "LabelNewestEpisodes": "Legújabb epizódok", + "LabelNewPassword": "Új jelszó", + "LabelNextBackupDate": "Következő biztonsági másolat dátuma", + "LabelNextScheduledRun": "Következő ütemezett futtatás", + "LabelNoEpisodesSelected": "Nincsenek kiválasztott epizódok", + "LabelNotes": "Megjegyzések", + "LabelNotFinished": "Nem befejezett", + "LabelNotificationAppriseURL": "Apprise URL(ek)", + "LabelNotificationAvailableVariables": "Elérhető változók", + "LabelNotificationBodyTemplate": "Törzs sablon", + "LabelNotificationEvent": "Értesítési esemény", + "LabelNotificationsMaxFailedAttempts": "Maximális sikertelen próbálkozások", + "LabelNotificationsMaxFailedAttemptsHelp": "Az értesítések akkor kerülnek letiltásra, ha ennyiszer nem sikerül elküldeni őket", + "LabelNotificationsMaxQueueSize": "Maximális értesítési események sorának mérete", + "LabelNotificationsMaxQueueSizeHelp": "Az események korlátozva vannak, hogy másodpercenként 1-szer történjenek. Ha a sor maximális méretű, akkor az események figyelmen kívül lesznek hagyva. Ez megakadályozza az értesítések spamelését.", + "LabelNotificationTitleTemplate": "Cím sablon", + "LabelNotStarted": "Nem indult el", + "LabelNumberOfBooks": "Könyvek száma", + "LabelNumberOfEpisodes": "Epizódok száma", + "LabelOpenRSSFeed": "RSS hírcsatorna megnyitása", + "LabelOverwrite": "Felülírás", + "LabelPassword": "Jelszó", + "LabelPath": "Útvonal", + "LabelPermissionsAccessAllLibraries": "Hozzáférhet az összes könyvtárhoz", + "LabelPermissionsAccessAllTags": "Hozzáférhet az összes címkéhez", + "LabelPermissionsAccessExplicitContent": "Hozzáférhet explicit tartalomhoz", + "LabelPermissionsDelete": "Törölhet", + "LabelPermissionsDownload": "Letölthet", + "LabelPermissionsUpdate": "Frissíthet", + "LabelPermissionsUpload": "Feltölthet", + "LabelPhotoPathURL": "Fénykép útvonal/URL", + "LabelPlaylists": "Lejátszási listák", + "LabelPlayMethod": "Lejátszási módszer", + "LabelPodcast": "Podcast", + "LabelPodcasts": "Podcastok", + "LabelPodcastSearchRegion": "Podcast keresési régió", + "LabelPodcastType": "Podcast típus", + "LabelPort": "Port", + "LabelPrefixesToIgnore": "Figyelmen kívül hagyandó előtagok (nem érzékeny a kis- és nagybetűkre)", + "LabelPreventIndexing": "A hírcsatorna indexelésének megakadályozása az iTunes és a Google podcast könyvtáraiban", + "LabelPrimaryEbook": "Elsődleges e-könyv", + "LabelProgress": "Haladás", + "LabelProvider": "Szolgáltató", + "LabelPubDate": "Kiadás dátuma", + "LabelPublisher": "Kiadó", + "LabelPublishYear": "Kiadás éve", + "LabelRead": "Olvasás", + "LabelReadAgain": "Újraolvasás", + "LabelReadEbookWithoutProgress": "E-könyv olvasása haladás nélkül", + "LabelRecentlyAdded": "Nemrég hozzáadva", + "LabelRecentSeries": "Legutóbbi sorozatok", + "LabelRecommended": "Ajánlott", + "LabelRedo": "Újra", + "LabelRegion": "Régió", + "LabelReleaseDate": "Megjelenés dátuma", + "LabelRemoveCover": "Borító eltávolítása", + "LabelRowsPerPage": "Sorok száma oldalanként", + "LabelRSSFeedCustomOwnerEmail": "Egyéni tulajdonos e-mail", + "LabelRSSFeedCustomOwnerName": "Egyéni tulajdonos neve", + "LabelRSSFeedOpen": "RSS hírcsatorna nyitva", + "LabelRSSFeedPreventIndexing": "Indexelés megakadályozása", + "LabelRSSFeedSlug": "RSS hírcsatorna slug", + "LabelRSSFeedURL": "RSS hírcsatorna URL", + "LabelSearchTerm": "Keresési kifejezés", + "LabelSearchTitle": "Cím keresése", + "LabelSearchTitleOrASIN": "Cím vagy ASIN keresése", + "LabelSeason": "Évad", + "LabelSelectAllEpisodes": "Összes epizód kiválasztása", + "LabelSelectEpisodesShowing": "Kiválasztás {0} megjelenített epizód", + "LabelSelectUsers": "Felhasználók kiválasztása", + "LabelSendEbookToDevice": "E-könyv küldése...", + "LabelSequence": "Sorozat", + "LabelSeries": "Sorozat", + "LabelSeriesName": "Sorozat neve", + "LabelSeriesProgress": "Sorozat haladása", + "LabelSetEbookAsPrimary": "Beállítás elsődlegesként", + "LabelSetEbookAsSupplementary": "Beállítás kiegészítőként", + "LabelSettingsAudiobooksOnly": "Csak hangoskönyvek", + "LabelSettingsAudiobooksOnlyHelp": "Ennek a beállításnak az engedélyezése figyelmen kívül hagyja az e-könyv fájlokat, kivéve, ha azok egy hangoskönyv mappában vannak, ebben az esetben kiegészítő e-könyvként lesznek beállítva", + "LabelSettingsBookshelfViewHelp": "Skeuomorfikus dizájn fa polcokkal", + "LabelSettingsChromecastSupport": "Chromecast támogatás", + "LabelSettingsDateFormat": "Dátumformátum", + "LabelSettingsDisableWatcher": "Figyelő letiltása", + "LabelSettingsDisableWatcherForLibrary": "Mappafigyelő letiltása a könyvtárban", + "LabelSettingsDisableWatcherHelp": "Letiltja az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges", + "LabelSettingsEnableWatcher": "Figyelő engedélyezése", + "LabelSettingsEnableWatcherForLibrary": "Mappafigyelő engedélyezése a könyvtárban", + "LabelSettingsEnableWatcherHelp": "Engedélyezi az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges", + "LabelSettingsExperimentalFeatures": "Kísérleti funkciók", + "LabelSettingsExperimentalFeaturesHelp": "Fejlesztés alatt álló funkciók, amelyek visszajelzésre és tesztelésre szorulnak. Kattintson a github megbeszélés megnyitásához.", + "LabelSettingsFindCovers": "Borítók keresése", + "LabelSettingsFindCoversHelp": "Ha a hangoskönyvnek nincs beágyazott borítója vagy borítóképe a mappában, a szkenner megpróbálja megtalálni a borítót.<br>Megjegyzés: Ez meghosszabbítja a szkennelési időt", + "LabelSettingsHideSingleBookSeries": "Egykönyves sorozatok elrejtése", + "LabelSettingsHideSingleBookSeriesHelp": "A csak egy könyvet tartalmazó sorozatok el lesznek rejtve a sorozatok oldalról és a kezdőlap polcairól.", + "LabelSettingsHomePageBookshelfView": "Kezdőlap használja a könyvespolc nézetet", + "LabelSettingsLibraryBookshelfView": "Könyvtár használja a könyvespolc nézetet", + "LabelSettingsParseSubtitles": "Feliratok elemzése", + "LabelSettingsParseSubtitlesHelp": "Feliratok kinyerése a hangoskönyv mappaneveiből.<br>A feliratnak el kell különülnie egy \" - \" jellel<br>például: \"Könyv címe - Egy felirat itt\" esetén a felirat \"Egy felirat itt\"", + "LabelSettingsPreferMatchedMetadata": "Preferált egyeztetett metaadatok", + "LabelSettingsPreferMatchedMetadataHelp": "Az egyeztetett adatok felülírják az elem részleteit a Gyors egyeztetés használatakor. Alapértelmezés szerint a Gyors egyeztetés csak a hiányzó részleteket tölti ki.", + "LabelSettingsSkipMatchingBooksWithASIN": "Már ASIN-nel rendelkező könyvek egyeztetésének kihagyása", + "LabelSettingsSkipMatchingBooksWithISBN": "Már ISBN-nel rendelkező könyvek egyeztetésének kihagyása", + "LabelSettingsSortingIgnorePrefixes": "Előtagok figyelmen kívül hagyása rendezéskor", + "LabelSettingsSortingIgnorePrefixesHelp": "például az \"a\" előtag esetén a \"A könyv címe\" könyv címe \"Könyv címe, A\" szerint rendeződik", + "LabelSettingsSquareBookCovers": "Négyzet alakú könyvborítók használata", + "LabelSettingsSquareBookCoversHelp": "Négyzet alakú borítók használata az 1,6:1 arányú standard könyvborítók helyett", + "LabelSettingsStoreCoversWithItem": "Borítók tárolása az elemmel", + "LabelSettingsStoreCoversWithItemHelp": "Alapértelmezés szerint a borítók a /metadata/items mappában vannak tárolva, ennek a beállításnak az engedélyezése a borítókat a könyvtári elem mappájában tárolja. Csak egy \"cover\" nevű fájl lesz megtartva", + "LabelSettingsStoreMetadataWithItem": "Metaadatok tárolása az elemmel", + "LabelSettingsStoreMetadataWithItemHelp": "Alapértelmezés szerint a metaadatfájlok a /metadata/items mappában vannak tárolva, ennek a beállításnak az engedélyezése a metaadatfájlokat a könyvtári elem mappáiban tárolja", + "LabelSettingsTimeFormat": "Időformátum", + "LabelShowAll": "Mindent mutat", + "LabelSize": "Méret", + "LabelSleepTimer": "Alvásidőzítő", + "LabelSlug": "Rövid cím", + "LabelStart": "Kezdés", + "LabelStarted": "Elkezdődött", + "LabelStartedAt": "Kezdés ideje", + "LabelStartTime": "Kezdési idő", + "LabelStatsAudioTracks": "Audiósávok", + "LabelStatsAuthors": "Szerzők", + "LabelStatsBestDay": "Legjobb nap", + "LabelStatsDailyAverage": "Napi átlag", + "LabelStatsDays": "Napok", + "LabelStatsDaysListened": "Hallgatott napok", + "LabelStatsHours": "Órák", + "LabelStatsInARow": "egymás után", + "LabelStatsItemsFinished": "Befejezett elemek", + "LabelStatsItemsInLibrary": "Elemek a könyvtárban", + "LabelStatsMinutes": "percek", + "LabelStatsMinutesListening": "Hallgatási percek", + "LabelStatsOverallDays": "Összes nap", + "LabelStatsOverallHours": "Összes óra", + "LabelStatsWeekListening": "Heti hallgatás", + "LabelSubtitle": "Felirat", + "LabelSupportedFileTypes": "Támogatott fájltípusok", + "LabelTag": "Címke", + "LabelTags": "Címkék", + "LabelTagsAccessibleToUser": "A felhasználó számára elérhető címkék", + "LabelTagsNotAccessibleToUser": "A felhasználó számára nem elérhető címkék", + "LabelTasks": "Futó feladatok", + "LabelTextEditorBulletedList": "Pontozott lista", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Számozott lista", + "LabelTextEditorUnlink": "Link eltávolítása", + "LabelTheme": "Téma", + "LabelThemeDark": "Sötét", + "LabelThemeLight": "Világos", + "LabelTimeBase": "Időalap", + "LabelTimeListened": "Hallgatott idő", + "LabelTimeListenedToday": "Ma hallgatott idő", + "LabelTimeRemaining": "{0} maradt", + "LabelTimeToShift": "Eltolás ideje másodpercben", + "LabelTitle": "Cím", + "LabelToolsEmbedMetadata": "Metaadatok beágyazása", + "LabelToolsEmbedMetadataDescription": "Metaadatok beágyazása az audiofájlokba, beleértve a borítóképet és a fejezeteket.", + "LabelToolsMakeM4b": "M4B Hangoskönyv fájl készítése", + "LabelToolsMakeM4bDescription": ".M4B hangoskönyv fájl generálása beágyazott metaadatokkal, borítóképpel és fejezetekkel.", + "LabelToolsSplitM4b": "M4B felosztása MP3-ra", + "LabelToolsSplitM4bDescription": "MP3 fájlok létrehozása egy M4B-ből, fejezetenként felosztva, beágyazott metaadatokkal, borítóképpel és fejezetekkel.", + "LabelTotalDuration": "Teljes időtartam", + "LabelTotalTimeListened": "Teljes hallgatási idő", + "LabelTrackFromFilename": "Sáv a fájlnévből", + "LabelTrackFromMetadata": "Sáv a metaadatokból", + "LabelTracks": "Sávok", + "LabelTracksMultiTrack": "Többsávos", + "LabelTracksNone": "Nincsenek sávok", + "LabelTracksSingleTrack": "Egysávos", + "LabelType": "Típus", + "LabelUnabridged": "Nem tömörített", + "LabelUndo": "Visszavonás", + "LabelUnknown": "Ismeretlen", + "LabelUpdateCover": "Borító frissítése", + "LabelUpdateCoverHelp": "Lehetővé teszi a meglévő borítók felülírását a kiválasztott könyveknél, amikor találatot talál", + "LabelUpdatedAt": "Frissítve", + "LabelUpdateDetails": "Részletek frissítése", + "LabelUpdateDetailsHelp": "Lehetővé teszi a meglévő részletek felülírását a kiválasztott könyveknél, amikor találatot talál", + "LabelUploaderDragAndDrop": "Fájlok vagy mappák húzása és elengedése", + "LabelUploaderDropFiles": "Fájlok elengedése", + "LabelUploaderItemFetchMetadataHelp": "Cím, szerző és sorozat automatikus lekérése", + "LabelUseChapterTrack": "Fejezetsáv használata", + "LabelUseFullTrack": "Teljes sáv használata", + "LabelUser": "Felhasználó", + "LabelUsername": "Felhasználónév", + "LabelValue": "Érték", + "LabelVersion": "Verzió", + "LabelViewBookmarks": "Könyvjelzők megtekintése", + "LabelViewChapters": "Fejezetek megtekintése", + "LabelViewQueue": "Lejátszó sor megtekintése", + "LabelVolume": "Hangerő", + "LabelWeekdaysToRun": "Futás napjai", + "LabelYourAudiobookDuration": "Hangoskönyv időtartama", + "LabelYourBookmarks": "Könyvjelzőid", + "LabelYourPlaylists": "Lejátszási listáid", + "LabelYourProgress": "Haladásod", + "MessageAddToPlayerQueue": "Hozzáadás a lejátszó sorhoz", + "MessageAppriseDescription": "Ennek a funkció használatához futtatnia kell egy <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> példányt vagy egy olyan API-t, amely kezeli ezeket a kéréseket. <br />Az Apprise API URL-nek a teljes URL útvonalat kell tartalmaznia az értesítés elküldéséhez, például, ha az API példánya a <code>http://192.168.1.1:8337</code> címen szolgáltatva, akkor <code>http://192.168.1.1:8337/notify</code> értéket kell megadnia.", + "MessageBackupsDescription": "A biztonsági másolatok tartalmazzák a felhasználókat, a felhasználói haladást, a könyvtári elem részleteit, a szerver beállításait és a képeket, amelyek a <code>/metadata/items</code> és <code>/metadata/authors</code> mappákban vannak tárolva. A biztonsági másolatok <strong>nem</strong> tartalmazzák a könyvtári mappákban tárolt fájlokat.", + "MessageBatchQuickMatchDescription": "A Gyors egyeztetés megpróbálja hozzáadni a hiányzó borítókat és metaadatokat a kiválasztott elemekhez. Engedélyezze az alábbi opciókat, hogy a Gyors egyeztetés felülírhassa a meglévő borítókat és/vagy metaadatokat.", + "MessageBookshelfNoCollections": "Még nem készített gyűjteményeket", + "MessageBookshelfNoResultsForFilter": "Nincs eredmény a \"{0}: {1}\" szűrőre", + "MessageBookshelfNoRSSFeeds": "Nincsenek nyitott RSS hírcsatornák", + "MessageBookshelfNoSeries": "Nincsenek sorozatai", + "MessageChapterEndIsAfter": "A fejezet vége a hangoskönyv végét követi", + "MessageChapterErrorFirstNotZero": "Az első fejezetnek 0:00-kor kell kezdődnie", + "MessageChapterErrorStartGteDuration": "Érvénytelen kezdési idő, kevesebbnek kell lennie, mint a hangoskönyv időtartama", + "MessageChapterErrorStartLtPrev": "Érvénytelen kezdési idő, nagyobbnak kell lennie, mint az előző fejezet kezdési ideje", + "MessageChapterStartIsAfter": "A fejezet kezdete a hangoskönyv végét követi", + "MessageCheckingCron": "Cron ellenőrzése...", + "MessageConfirmCloseFeed": "Biztosan be szeretné zárni ezt a hírcsatornát?", + "MessageConfirmDeleteBackup": "Biztosan törölni szeretné a(z) {0} biztonsági másolatot?", + "MessageConfirmDeleteFile": "Ez törölni fogja a fájlt a fájlrendszerből. Biztos benne?", + "MessageConfirmDeleteLibrary": "Biztosan véglegesen törölni szeretné a(z) \"{0}\" könyvtárat?", + "MessageConfirmDeleteLibraryItem": "Ez eltávolítja a könyvtári elemet az adatbázisból és a fájlrendszerből. Biztos benne?", + "MessageConfirmDeleteLibraryItems": "Ez eltávolítja a(z) {0} könyvtári elemet az adatbázisból és a fájlrendszerből. Biztos benne?", + "MessageConfirmDeleteSession": "Biztosan törölni szeretné ezt a munkamenetet?", + "MessageConfirmForceReScan": "Biztosan kényszeríteni szeretné az újraszkennelést?", + "MessageConfirmMarkAllEpisodesFinished": "Biztosan meg szeretné jelölni az összes epizódot befejezettnek?", + "MessageConfirmMarkAllEpisodesNotFinished": "Biztosan meg szeretné jelölni az összes epizódot nem befejezettnek?", + "MessageConfirmMarkSeriesFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét befejezettnek?", + "MessageConfirmMarkSeriesNotFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét nem befejezettnek?", + "MessageConfirmQuickEmbed": "Figyelem! A Gyors beágyazás nem készít biztonsági másolatot az audiofájlokról. Győződjön meg arról, hogy van biztonsági másolata az audiofájlokról. <br><br>Szeretné folytatni?", + "MessageConfirmRemoveAllChapters": "Biztosan eltávolítja az összes fejezetet?", + "MessageConfirmRemoveAuthor": "Biztosan eltávolítja a(z) \"{0}\" szerzőt?", + "MessageConfirmRemoveCollection": "Biztosan eltávolítja a(z) \"{0}\" gyűjteményt?", + "MessageConfirmRemoveEpisode": "Biztosan eltávolítja a(z) \"{0}\" epizódot?", + "MessageConfirmRemoveEpisodes": "Biztosan eltávolítja a(z) {0} epizódot?", + "MessageConfirmRemoveListeningSessions": "Biztosan eltávolítja a(z) {0} hallgatási munkamenetet?", + "MessageConfirmRemoveNarrator": "Biztosan eltávolítja a(z) \"{0}\" előadót?", + "MessageConfirmRemovePlaylist": "Biztosan eltávolítja a(z) \"{0}\" lejátszási listáját?", + "MessageConfirmRenameGenre": "Biztosan át szeretné nevezni a(z) \"{0}\" műfajt \"{1}\"-re az összes elemnél?", + "MessageConfirmRenameGenreMergeNote": "Megjegyzés: Ez a műfaj már létezik, így össze lesznek vonva.", + "MessageConfirmRenameGenreWarning": "Figyelem! Egy hasonló, de eltérő nagybetűkkel rendelkező műfaj már létezik \"{0}\".", + "MessageConfirmRenameTag": "Biztosan át szeretné nevezni a(z) \"{0}\" címkét \"{1}\"-re az összes elemnél?", + "MessageConfirmRenameTagMergeNote": "Megjegyzés: Ez a címke már létezik, így össze lesznek vonva.", + "MessageConfirmRenameTagWarning": "Figyelem! Egy hasonló, de eltérő nagybetűkkel rendelkező címke már létezik \"{0}\".", + "MessageConfirmReScanLibraryItems": "Biztosan újra szeretné szkennelni a(z) {0} elemet?", + "MessageConfirmSendEbookToDevice": "Biztosan el szeretné küldeni a(z) {0} e-könyvet a(z) \"{1}\" eszközre?", + "MessageDownloadingEpisode": "Epizód letöltése", + "MessageDragFilesIntoTrackOrder": "Húzza a fájlokat a helyes sávrendbe", + "MessageEmbedFinished": "Beágyazás befejeződött!", + "MessageEpisodesQueuedForDownload": "{0} Epizód letöltésre várakozik", + "MessageFeedURLWillBe": "A hírcsatorna URL-je {0} lesz", + "MessageFetching": "Lekérés...", + "MessageForceReScanDescription": "minden fájlt újra szkennel, mint egy friss szkennelés. Az audiofájlok ID3 címkéi, OPF fájlok és szövegfájlok újként lesznek szkennelve.", + "MessageImportantNotice": "Fontos közlemény!", + "MessageInsertChapterBelow": "Fejezet beszúrása alulra", + "MessageItemsSelected": "{0} kiválasztott elem", + "MessageItemsUpdated": "{0} frissített elem", + "MessageJoinUsOn": "Csatlakozzon hozzánk: ", + "MessageListeningSessionsInTheLastYear": "{0} hallgatási munkamenet az elmúlt évben", + "MessageLoading": "Betöltés...", + "MessageLoadingFolders": "Mappák betöltése...", + "MessageM4BFailed": "M4B sikertelen!", + "MessageM4BFinished": "M4B befejeződött!", + "MessageMapChapterTitles": "Fejezetcímek hozzárendelése a meglévő hangoskönyv fejezeteihez anélkül, hogy az időbélyegeket módosítaná", + "MessageMarkAllEpisodesFinished": "Az összes epizód megjelölése befejezettnek", + "MessageMarkAllEpisodesNotFinished": "Az összes epizód megjelölése nem befejezettnek", + "MessageMarkAsFinished": "Megjelölés befejezettnek", + "MessageMarkAsNotFinished": "Megjelölés nem befejezettnek", + "MessageMatchBooksDescription": "megpróbálja egyeztetni a könyvtár könyveit egy kiválasztott keresési szolgáltató könyvével, és kitölti az üres részleteket és a borítót. Nem írja felül a részleteket.", + "MessageNoAudioTracks": "Nincsenek audiósávok", + "MessageNoAuthors": "Nincsenek szerzők", + "MessageNoBackups": "Nincsenek biztonsági másolatok", + "MessageNoBookmarks": "Nincsenek könyvjelzők", + "MessageNoChapters": "Nincsenek fejezetek", + "MessageNoCollections": "Nincsenek gyűjtemények", + "MessageNoCoversFound": "Nem találhatóak borítók", + "MessageNoDescription": "Nincs leírás", + "MessageNoDownloadsInProgress": "Jelenleg nincsenek folyamatban lévő letöltések", + "MessageNoDownloadsQueued": "Nincsenek várakozó letöltések", + "MessageNoEpisodeMatchesFound": "Nincs találat az epizódokra", + "MessageNoEpisodes": "Nincsenek epizódok", + "MessageNoFoldersAvailable": "Nincsenek elérhető mappák", + "MessageNoGenres": "Nincsenek műfajok", + "MessageNoIssues": "Nincsenek problémák", + "MessageNoItems": "Nincsenek elemek", + "MessageNoItemsFound": "Nem találhatóak elemek", + "MessageNoListeningSessions": "Nincsenek hallgatási munkamenetek", + "MessageNoLogs": "Nincsenek naplók", + "MessageNoMediaProgress": "Nincs előrehaladás a médialejátszásban", + "MessageNoNotifications": "Nincsenek értesítések", + "MessageNoPodcastsFound": "Nem találhatóak podcastok", + "MessageNoResults": "Nincsenek eredmények", + "MessageNoSearchResultsFor": "Nincs keresési eredmény erre: \"{0}\"", + "MessageNoSeries": "Nincsenek sorozatok", + "MessageNoTags": "Nincsenek címkék", + "MessageNoTasksRunning": "Nincsenek futó feladatok", + "MessageNotYetImplemented": "Még nem implementált", + "MessageNoUpdateNecessary": "Nincs szükség frissítésre", + "MessageNoUpdatesWereNecessary": "Nem volt szükség frissítésekre", + "MessageNoUserPlaylists": "Nincsenek felhasználói lejátszási listák", + "MessageOr": "vagy", + "MessagePauseChapter": "Fejezet lejátszásának szüneteltetése", + "MessagePlayChapter": "Fejezet elejének meghallgatása", + "MessagePlaylistCreateFromCollection": "Lejátszási lista létrehozása gyűjteményből", + "MessagePodcastHasNoRSSFeedForMatching": "A podcastnak nincs RSS hírcsatorna URL-je az egyeztetéshez", + "MessageQuickMatchDescription": "Üres elem részletek és borító feltöltése az első találati eredménnyel a(z) '{0}'-ból. Nem írja felül a részleteket, kivéve, ha a 'Preferált egyeztetett metaadatok' szerverbeállítás engedélyezve van.", + "MessageRemoveChapter": "Fejezet eltávolítása", + "MessageRemoveEpisodes": "Epizód(ok) eltávolítása: {0}", + "MessageRemoveFromPlayerQueue": "Eltávolítás a lejátszási sorból", + "MessageRemoveUserWarning": "Biztosan véglegesen törölni szeretné a(z) \"{0}\" felhasználót?", + "MessageReportBugsAndContribute": "Hibák jelentése, funkciók kérése és hozzájárulás itt:", + "MessageResetChaptersConfirm": "Biztosan alaphelyzetbe szeretné állítani a fejezeteket és visszavonni a módosításokat?", + "MessageRestoreBackupConfirm": "Biztosan vissza szeretné állítani a biztonsági másolatot, amely ekkor készült:", + "MessageRestoreBackupWarning": "A biztonsági mentés visszaállítása felülírja az egész adatbázist, amely a /config mappában található, valamint a borítóképeket a /metadata/items és /metadata/authors mappákban.<br /><br />A biztonsági mentések nem módosítják a könyvtár mappáiban található fájlokat. Ha engedélyezte a szerverbeállításokat a borítóképek és a metaadatok könyvtármappákban való tárolására, akkor ezek nem kerülnek biztonsági mentésre vagy felülírásra.<br /><br />A szerver használó összes kliens automatikusan frissül.", + "MessageSearchResultsFor": "Keresési eredmények", + "MessageSelected": "{0} kiválasztva", + "MessageServerCouldNotBeReached": "A szervert nem lehet elérni", + "MessageSetChaptersFromTracksDescription": "Fejezetek beállítása minden egyes hangfájlt egy fejezetként használva, és a fejezet címét a hangfájl neveként", + "MessageStartPlaybackAtTime": "\"{0}\" lejátszásának kezdése {1} -tól?", + "MessageThinking": "Gondolkodás...", + "MessageUploaderItemFailed": "A feltöltés sikertelen", + "MessageUploaderItemSuccess": "Sikeresen feltöltve!", + "MessageUploading": "Feltöltés...", + "MessageValidCronExpression": "Érvényes cron kifejezés", + "MessageWatcherIsDisabledGlobally": "A megfigyelő globálisan le van tiltva a szerver beállításokban", + "MessageXLibraryIsEmpty": "{0} könyvtár üres!", + "MessageYourAudiobookDurationIsLonger": "Az Ön hangoskönyvének hossza hosszabb, mint a talált időtartam", + "MessageYourAudiobookDurationIsShorter": "Az Ön hangoskönyvének hossza rövidebb, mint a talált időtartam", + "NoteChangeRootPassword": "A Root felhasználó az egyetlen felhasználó, akinek lehet üres jelszava", + "NoteChapterEditorTimes": "Megjegyzés: Az első fejezet kezdőidejének 0:00 kell lennie, és az utolsó fejezet kezdőideje nem haladhatja meg a hangoskönyv időtartamát.", + "NoteFolderPicker": "Megjegyzés: azok a mappák, amelyek már hozzá vannak rendelve, nem jelennek meg", + "NoteRSSFeedPodcastAppsHttps": "Figyelem: A legtöbb podcast alkalmazás megköveteli, hogy az RSS feed URL HTTPS-t használjon", + "NoteRSSFeedPodcastAppsPubDate": "Figyelem: Az egy vagy több epizódnak nincs Közzétételi dátuma. Néhány podcast alkalmazás ezt megköveteli.", + "NoteUploaderFoldersWithMediaFiles": "A médiafájlokat tartalmazó mappák külön könyvtári tételekként lesznek kezelve.", + "NoteUploaderOnlyAudioFiles": "Ha csak hangfájlokat tölt fel, akkor minden egyes hangfájl külön hangoskönyvként lesz kezelve.", + "NoteUploaderUnsupportedFiles": "A nem támogatott fájlok figyelmen kívül hagyásra kerülnek. Mappa kiválasztása vagy elengedésekor az elem mappáján kívüli egyéb fájlok figyelmen kívül lesznek hagyva.", + "PlaceholderNewCollection": "Új gyűjtemény neve", + "PlaceholderNewFolderPath": "Új mappa útvonala", + "PlaceholderNewPlaylist": "Új lejátszási lista neve", + "PlaceholderSearch": "Keresés..", + "PlaceholderSearchEpisode": "Epizód keresése..", + "ToastAccountUpdateFailed": "A fiók frissítése sikertelen", + "ToastAccountUpdateSuccess": "Fiók frissítve", + "ToastAuthorImageRemoveFailed": "A kép eltávolítása sikertelen", + "ToastAuthorImageRemoveSuccess": "Szerző képe eltávolítva", + "ToastAuthorUpdateFailed": "A szerző frissítése sikertelen", + "ToastAuthorUpdateMerged": "Szerző összevonva", + "ToastAuthorUpdateSuccess": "Szerző frissítve", + "ToastAuthorUpdateSuccessNoImageFound": "Szerző frissítve (nem található kép)", + "ToastBackupCreateFailed": "A biztonsági mentés létrehozása sikertelen", + "ToastBackupCreateSuccess": "Biztonsági mentés létrehozva", + "ToastBackupDeleteFailed": "A biztonsági mentés törlése sikertelen", + "ToastBackupDeleteSuccess": "Biztonsági mentés törölve", + "ToastBackupRestoreFailed": "A biztonsági mentés visszaállítása sikertelen", + "ToastBackupUploadFailed": "A biztonsági mentés feltöltése sikertelen", + "ToastBackupUploadSuccess": "Biztonsági mentés feltöltve", + "ToastBatchUpdateFailed": "Kötegelt frissítés sikertelen", + "ToastBatchUpdateSuccess": "Kötegelt frissítés sikeres", + "ToastBookmarkCreateFailed": "Könyvjelző létrehozása sikertelen", + "ToastBookmarkCreateSuccess": "Könyvjelző hozzáadva", + "ToastBookmarkRemoveFailed": "Könyvjelző eltávolítása sikertelen", + "ToastBookmarkRemoveSuccess": "Könyvjelző eltávolítva", + "ToastBookmarkUpdateFailed": "Könyvjelző frissítése sikertelen", + "ToastBookmarkUpdateSuccess": "Könyvjelző frissítve", + "ToastChaptersHaveErrors": "A fejezetek hibákat tartalmaznak", + "ToastChaptersMustHaveTitles": "A fejezeteknek címekkel kell rendelkezniük", + "ToastCollectionItemsRemoveFailed": "Elem(ek) eltávolítása a gyűjteményből sikertelen", + "ToastCollectionItemsRemoveSuccess": "Elem(ek) eltávolítva a gyűjteményből", + "ToastCollectionRemoveFailed": "Gyűjtemény eltávolítása sikertelen", + "ToastCollectionRemoveSuccess": "Gyűjtemény eltávolítva", + "ToastCollectionUpdateFailed": "Gyűjtemény frissítése sikertelen", + "ToastCollectionUpdateSuccess": "Gyűjtemény frissítve", + "ToastItemCoverUpdateFailed": "Elem borítójának frissítése sikertelen", + "ToastItemCoverUpdateSuccess": "Elem borítója frissítve", + "ToastItemDetailsUpdateFailed": "Elem részleteinek frissítése sikertelen", + "ToastItemDetailsUpdateSuccess": "Elem részletei frissítve", + "ToastItemDetailsUpdateUnneeded": "Nincsenek szükséges frissítések a tétel részletein", + "ToastItemMarkedAsFinishedFailed": "Megjelölés Befejezettként sikertelen", + "ToastItemMarkedAsFinishedSuccess": "Elem megjelölve Befejezettként", + "ToastItemMarkedAsNotFinishedFailed": "Nem sikerült Nem Befejezettként megjelölni az elemet", + "ToastItemMarkedAsNotFinishedSuccess": "Elem megjelölve Nem Befejezettként", + "ToastLibraryCreateFailed": "Könyvtár létrehozása sikertelen", + "ToastLibraryCreateSuccess": "\"{0}\" könyvtár létrehozva", + "ToastLibraryDeleteFailed": "Könyvtár törlése sikertelen", + "ToastLibraryDeleteSuccess": "Könyvtár törölve", + "ToastLibraryScanFailedToStart": "A beolvasás elindítása sikertelen", + "ToastLibraryScanStarted": "Könyvtár beolvasása elindítva", + "ToastLibraryUpdateFailed": "Könyvtár frissítése sikertelen", + "ToastLibraryUpdateSuccess": "\"{0}\" könyvtár frissítve", + "ToastPlaylistCreateFailed": "Lejátszási lista létrehozása sikertelen", + "ToastPlaylistCreateSuccess": "Lejátszási lista létrehozva", + "ToastPlaylistRemoveFailed": "Lejátszási lista eltávolítása sikertelen", + "ToastPlaylistRemoveSuccess": "Lejátszási lista eltávolítva", + "ToastPlaylistUpdateFailed": "Lejátszási lista frissítése sikertelen", + "ToastPlaylistUpdateSuccess": "Lejátszási lista frissítve", + "ToastPodcastCreateFailed": "Podcast létrehozása sikertelen", + "ToastPodcastCreateSuccess": "Podcast sikeresen létrehozva", + "ToastRemoveItemFromCollectionFailed": "Tétel eltávolítása a gyűjteményből sikertelen", + "ToastRemoveItemFromCollectionSuccess": "Tétel eltávolítva a gyűjteményből", + "ToastRSSFeedCloseFailed": "RSS feed bezárása sikertelen", + "ToastRSSFeedCloseSuccess": "RSS feed bezárva", + "ToastSendEbookToDeviceFailed": "E-könyv küldése az eszközre sikertelen", + "ToastSendEbookToDeviceSuccess": "E-könyv elküldve az eszközre \"{0}\"", + "ToastSeriesUpdateFailed": "Sorozat frissítése sikertelen", + "ToastSeriesUpdateSuccess": "Sorozat frissítése sikeres", + "ToastSessionDeleteFailed": "Munkamenet törlése sikertelen", + "ToastSessionDeleteSuccess": "Munkamenet törölve", + "ToastSocketConnected": "Socket csatlakoztatva", + "ToastSocketDisconnected": "Socket lecsatlakoztatva", + "ToastSocketFailedToConnect": "A Socket csatlakoztatása sikertelen", + "ToastUserDeleteFailed": "Felhasználó törlése sikertelen", + "ToastUserDeleteSuccess": "Felhasználó törölve" +} \ No newline at end of file From 3d66ec0761186f90f0179e1953b9c2e941d73752 Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:42:48 -0300 Subject: [PATCH 0390/2145] PT-BR size adjustment --- client/strings/pt-br.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 5bb91aed..10dcdc22 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -122,12 +122,12 @@ "HeaderFindChapters": "Localizar Capítulos", "HeaderIgnoredFiles": "Arquivos Ignorados", "HeaderItemFiles": "Arquivos de Itens", - "HeaderItemMetadataUtils": "Utilidades para Metadados de Itens", + "HeaderItemMetadataUtils": "Utilidades para Metadados dos Itens", "HeaderLastListeningSession": "Última sessão", "HeaderLatestEpisodes": "Últimos episódios", "HeaderLibraries": "Bibliotecas", "HeaderLibraryFiles": "Arquivos da Biblioteca", - "HeaderLibraryStats": "Estatisticas da Biblioteca", + "HeaderLibraryStats": "Estatísticas da Biblioteca", "HeaderListeningSessions": "Sessões", "HeaderListeningStats": "Estatísticas", "HeaderLogin": "Login", @@ -396,7 +396,7 @@ "LabelPlayMethod": "Método de Reprodução", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", - "LabelPodcastSearchRegion": "Região para pesquisa de podcast", + "LabelPodcastSearchRegion": "Podcast search region", "LabelPodcastType": "Tipo de Podcast", "LabelPort": "Porta", "LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)", @@ -410,7 +410,7 @@ "LabelRead": "Lido", "LabelReadAgain": "Ler novamente", "LabelReadEbookWithoutProgress": "Ler ebook sem armazenar progresso", - "LabelRecentlyAdded": "Recentemente Acrescentado", + "LabelRecentlyAdded": "Novidades", "LabelRecentSeries": "Séries Recentes", "LabelRecommended": "Recomendado", "LabelRedo": "Refazer", From 9f848b2c6462a822315c5aa06f036f21854f044a Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:28:37 -0300 Subject: [PATCH 0391/2145] Update pt-br.json - adjust terminology and typo --- client/strings/pt-br.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 10dcdc22..fc626aff 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -106,7 +106,7 @@ "HeaderChapters": "Capítulos", "HeaderChooseAFolder": "Escolha uma Pasta", "HeaderCollection": "Coleção", - "HeaderCollectionItems": "Itends da Coleção", + "HeaderCollectionItems": "Itens da Coleção", "HeaderCover": "Capas", "HeaderCurrentDownloads": "Downloads em andamento", "HeaderCustomMetadataProviders": "Fontes de Metadados Customizados", @@ -230,10 +230,10 @@ "LabelClickForMoreInfo": "Clique para mais informações", "LabelClosePlayer": "Fechar Reprodutor", "LabelCodec": "Codec", - "LabelCollapseSeries": "Fechar Séries", + "LabelCollapseSeries": "Fechar Série", "LabelCollection": "Coleção", "LabelCollections": "Coleções", - "LabelComplete": "Completo", + "LabelComplete": "Concluído", "LabelConfirmPassword": "Confirmar Senha", "LabelContinueListening": "Continuar Escutando", "LabelContinueReading": "Continuar Lendo", @@ -471,8 +471,8 @@ "LabelSettingsStoreCoversWithItemHelp": "Por padrão as capas são armazenadas em /metadata/items. Ao ativar essa configuração as capas serão armazenadas na pasta do item na sua biblioteca. Apenas um arquivo chamado \"cover\" será mantido", "LabelSettingsStoreMetadataWithItem": "Armazenar metadados com o item", "LabelSettingsStoreMetadataWithItemHelp": "Por padrão os arquivos de metadados são armazenados em /metadata/items. Ao ativar essa configuração os arquivos de metadados serão armazenadas nas pastas dos itens na sua biblioteca", - "LabelSettingsTimeFormat": "Formato do Tempo", - "LabelShowAll": "Mostrar Todos", + "LabelSettingsTimeFormat": "Formato da Tempo", + "LabelShowAll": "Exibir Todos", "LabelSize": "Tamanho", "LabelSleepTimer": "Timer", "LabelSlug": "Slug", @@ -487,7 +487,7 @@ "LabelStatsDays": "Dias", "LabelStatsDaysListened": "Dias Escutando", "LabelStatsHours": "Horas", - "LabelStatsInARow": "seguidas", + "LabelStatsInARow": "seguidos", "LabelStatsItemsFinished": "itens Concluídos", "LabelStatsItemsInLibrary": "itens na biblioteca", "LabelStatsMinutes": "minutos", @@ -643,7 +643,7 @@ "MessageNoLogs": "Sem Logs", "MessageNoMediaProgress": "Sem Progresso de Mídia", "MessageNoNotifications": "Sem Notificações", - "MessageNoPodcastsFound": "Nenhum podcasts encontrado", + "MessageNoPodcastsFound": "Nenhum podcast encontrado", "MessageNoResults": "Sem resultados", "MessageNoSearchResultsFor": "Sem resultados para \"{0}\"", "MessageNoSeries": "Sem Séries", From c07b7840e2a749969d143d8142f9af27aad741e5 Mon Sep 17 00:00:00 2001 From: DownloadableFox <me@downloadablefox.dev> Date: Tue, 20 Feb 2024 21:14:55 -0500 Subject: [PATCH 0392/2145] Updated and fixed Spanish translation --- client/strings/es.json | 122 ++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/client/strings/es.json b/client/strings/es.json index f2714c00..4b85106c 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -1,11 +1,11 @@ { "ButtonAdd": "Agregar", "ButtonAddChapters": "Agregar Capitulo", - "ButtonAddDevice": "Add Device", - "ButtonAddLibrary": "Add Library", + "ButtonAddDevice": "Agregar Dispositivo", + "ButtonAddLibrary": "Crear Biblioteca", "ButtonAddPodcasts": "Agregar Podcasts", - "ButtonAddUser": "Add User", - "ButtonAddYourFirstLibrary": "Agrega tu Primera Biblioteca", + "ButtonAddUser": "Crear Usuario", + "ButtonAddYourFirstLibrary": "Crea tu Primera Biblioteca", "ButtonApply": "Aplicar", "ButtonApplyChapters": "Aplicar Capítulos", "ButtonAuthors": "Autores", @@ -32,8 +32,8 @@ "ButtonHide": "Esconder", "ButtonHome": "Inicio", "ButtonIssues": "Problemas", - "ButtonJumpBackward": "Jump Backward", - "ButtonJumpForward": "Jump Forward", + "ButtonJumpBackward": "Retroceder", + "ButtonJumpForward": "Adelantar", "ButtonLatest": "Últimos", "ButtonLibrary": "Biblioteca", "ButtonLogout": "Cerrar Sesión", @@ -43,15 +43,15 @@ "ButtonMatchAllAuthors": "Encontrar Todos los Autores", "ButtonMatchBooks": "Encontrar Libros", "ButtonNevermind": "Olvidar", - "ButtonNextChapter": "Next Chapter", + "ButtonNextChapter": "Siguiente Capítulo", "ButtonOk": "Ok", "ButtonOpenFeed": "Abrir Fuente", "ButtonOpenManager": "Abrir Editor", - "ButtonPause": "Pause", + "ButtonPause": "Pausar", "ButtonPlay": "Reproducir", "ButtonPlaying": "Reproduciendo", "ButtonPlaylists": "Listas de Reproducción", - "ButtonPreviousChapter": "Previous Chapter", + "ButtonPreviousChapter": "Capítulo Anterior", "ButtonPurgeAllCache": "Purgar Todo el Cache", "ButtonPurgeItemsCache": "Purgar Elementos de Cache", "ButtonPurgeMediaProgress": "Purgar Progreso de Multimedia", @@ -92,15 +92,15 @@ "ButtonUserEdit": "Editar Usuario {0}", "ButtonViewAll": "Ver Todos", "ButtonYes": "Aceptar", - "ErrorUploadFetchMetadataAPI": "Error fetching metadata", - "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", - "ErrorUploadLacksTitle": "Must have a title", + "ErrorUploadFetchMetadataAPI": "Error obteniendo metadatos", + "ErrorUploadFetchMetadataNoResults": "No se pudo obtener metadatos - Intenta actualizar el título y/o autor", + "ErrorUploadLacksTitle": "Se debe tener título", "HeaderAccount": "Cuenta", "HeaderAdvanced": "Avanzado", "HeaderAppriseNotificationSettings": "Ajustes de Notificaciones de Apprise", "HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro", "HeaderAudioTracks": "Pistas de Audio", - "HeaderAuthentication": "Authentication", + "HeaderAuthentication": "Autenticación", "HeaderBackups": "Respaldos", "HeaderChangePassword": "Cambiar Contraseña", "HeaderChapters": "Capítulos", @@ -109,7 +109,7 @@ "HeaderCollectionItems": "Elementos en la Colección", "HeaderCover": "Portada", "HeaderCurrentDownloads": "Descargando Actualmente", - "HeaderCustomMetadataProviders": "Custom Metadata Providers", + "HeaderCustomMetadataProviders": "Proveedores de metadatos personalizados", "HeaderDetails": "Detalles", "HeaderDownloadQueue": "Lista de Descarga", "HeaderEbookFiles": "Archivos de Ebook", @@ -136,15 +136,15 @@ "HeaderManageTags": "Administrar Etiquetas", "HeaderMapDetails": "Asignar Detalles", "HeaderMatch": "Encontrar", - "HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", + "HeaderMetadataOrderOfPrecedence": "Orden de precedencia de metadatos", "HeaderMetadataToEmbed": "Metadatos para Insertar", "HeaderNewAccount": "Nueva Cuenta", "HeaderNewLibrary": "Nueva Biblioteca", "HeaderNotifications": "Notificaciones", - "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", + "HeaderOpenIDConnectAuthentication": "Autenticación OpenID Connect", "HeaderOpenRSSFeed": "Abrir fuente RSS", "HeaderOtherFiles": "Otros Archivos", - "HeaderPasswordAuthentication": "Password Authentication", + "HeaderPasswordAuthentication": "Autenticación por contraseña", "HeaderPermissions": "Permisos", "HeaderPlayerQueue": "Fila del Reproductor", "HeaderPlaylist": "Lista de Reproducción", @@ -193,11 +193,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", + "LabelAdminUsersOnly": "Solamente usuarios administradores", "LabelAll": "Todos", "LabelAllUsers": "Todos los Usuarios", - "LabelAllUsersExcludingGuests": "All users excluding guests", - "LabelAllUsersIncludingGuests": "All users including guests", + "LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados", + "LabelAllUsersIncludingGuests": "Todos los usuarios e invitados", "LabelAlreadyInYourLibrary": "Ya en la Biblioteca", "LabelAppend": "Adjuntar", "LabelAuthor": "Autor", @@ -205,12 +205,12 @@ "LabelAuthorLastFirst": "Autor (Apellido, Nombre)", "LabelAuthors": "Autores", "LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente", - "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", - "LabelAutoRegisterDescription": "Automatically create new users after logging in", + "LabelAutoFetchMetadata": "Actualizar Metadatos Automáticamente", + "LabelAutoFetchMetadataHelp": "Obtiene metadatos de título, autor y serie para agilizar la carga. Es posible que haya que cotejar metadatos adicionales después de la carga.", + "LabelAutoLaunch": "Lanzamiento automático", + "LabelAutoLaunchDescription": "Redirigir al proveedor de autenticación automáticamente al navegar a la página de inicio de sesión (ruta de sobreescritura manual <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Registro automático", + "LabelAutoRegisterDescription": "Crear usuarios automáticamente tras iniciar sesión", "LabelBackToUser": "Regresar a Usuario", "LabelBackupLocation": "Ubicación del Respaldo", "LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático", @@ -221,13 +221,13 @@ "LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.", "LabelBitrate": "Bitrate", "LabelBooks": "Libros", - "LabelButtonText": "Button Text", + "LabelButtonText": "Texto del botón", "LabelChangePassword": "Cambiar Contraseña", "LabelChannels": "Canales", "LabelChapters": "Capítulos", "LabelChaptersFound": "Capítulo Encontrado", "LabelChapterTitle": "Titulo del Capítulo", - "LabelClickForMoreInfo": "Click for more info", + "LabelClickForMoreInfo": "Click para más información", "LabelClosePlayer": "Cerrar Reproductor", "LabelCodec": "Codec", "LabelCollapseSeries": "Colapsar Serie", @@ -246,12 +246,12 @@ "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)", + "LabelDeleteFromFileSystemCheckbox": "Eliminar archivos del sistema (desmarcar para eliminar sólo de la base de datos)", "LabelDescription": "Descripción", "LabelDeselectAll": "Deseleccionar Todos", "LabelDevice": "Dispositivo", "LabelDeviceInfo": "Información de Dispositivo", - "LabelDeviceIsAvailableTo": "Device is available to...", + "LabelDeviceIsAvailableTo": "El dispositivo está disponible para...", "LabelDirectory": "Directorio", "LabelDiscFromFilename": "Disco a partir del Nombre del Archivo", "LabelDiscFromMetadata": "Disco a partir de Metadata", @@ -277,7 +277,7 @@ "LabelExample": "Ejemplo", "LabelExplicit": "Explicito", "LabelFeedURL": "Fuente de URL", - "LabelFetchingMetadata": "Fetching Metadata", + "LabelFetchingMetadata": "Obteniendo metadatos", "LabelFile": "Archivo", "LabelFileBirthtime": "Archivo Creado en", "LabelFileModified": "Archivo modificado", @@ -287,23 +287,23 @@ "LabelFinished": "Terminado", "LabelFolder": "Carpeta", "LabelFolders": "Carpetas", - "LabelFontBold": "Bold", + "LabelFontBold": "Negrilla", "LabelFontFamily": "Familia tipográfica", - "LabelFontItalic": "Italic", + "LabelFontItalic": "Itálica", "LabelFontScale": "Tamaño de Fuente", - "LabelFontStrikethrough": "Strikethrough", + "LabelFontStrikethrough": "Tachado", "LabelFormat": "Formato", "LabelGenre": "Genero", "LabelGenres": "Géneros", "LabelHardDeleteFile": "Eliminar Definitivamente", "LabelHasEbook": "Tiene Ebook", "LabelHasSupplementaryEbook": "Tiene Ebook Suplementario", - "LabelHighestPriority": "Highest priority", + "LabelHighestPriority": "Mayor prioridad", "LabelHost": "Host", "LabelHour": "Hora", "LabelIcon": "Icono", - "LabelImageURLFromTheWeb": "Image URL from the web", - "LabelIncludeInTracklist": "Incluir en Tracklist", + "LabelImageURLFromTheWeb": "URL de la imagen", + "LabelIncludeInTracklist": "Incluir en la Tracklist", "LabelIncomplete": "Incompleto", "LabelInProgress": "En Proceso", "LabelInterval": "Intervalo", @@ -340,20 +340,20 @@ "LabelLogLevelInfo": "Información", "LabelLogLevelWarn": "Advertencia", "LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha", - "LabelLowestPriority": "Lowest Priority", - "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", + "LabelLowestPriority": "Menor prioridad", + "LabelMatchExistingUsersBy": "Emparejar a los usuarios existentes por", + "LabelMatchExistingUsersByDescription": "Se utiliza para conectar usuarios existentes. Una vez conectados, los usuarios serán emparejados por un identificador único de su proveedor de SSO", "LabelMediaPlayer": "Reproductor de Medios", "LabelMediaType": "Tipo de Multimedia", - "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", - "LabelMetadataProvider": "Proveedor de Metadata", - "LabelMetaTag": "Meta Tag", - "LabelMetaTags": "Meta Tags", + "LabelMetadataOrderOfPrecedenceDescription": "Las fuentes de metadatos de mayor prioridad prevalecerán sobre las de menor prioridad", + "LabelMetadataProvider": "Proveedor de Metadatos", + "LabelMetaTag": "Metaetiqueta", + "LabelMetaTags": "Metaetiquetas", "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.", + "LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos", + "LabelMobileRedirectURIsDescription": "Esta es una lista de URIs válidos para redireccionamiento de apps móviles. La URI por defecto es <code>audiobookshelf://oauth</code>, la cual puedes remover or corroborar con URIs adicionales para la integración con apps de terceros. Utilizando un asterisco (<code>*</code>) como el único punto de entrada permite cualquier URI.", "LabelMore": "Más", "LabelMoreInfo": "Más Información", "LabelName": "Nombre", @@ -413,11 +413,11 @@ "LabelRecentlyAdded": "Agregado Recientemente", "LabelRecentSeries": "Series Recientes", "LabelRecommended": "Recomendados", - "LabelRedo": "Redo", + "LabelRedo": "Rehacer", "LabelRegion": "Región", "LabelReleaseDate": "Fecha de Estreno", "LabelRemoveCover": "Remover Portada", - "LabelRowsPerPage": "Rows per page", + "LabelRowsPerPage": "Filas por página", "LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado", "LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado", "LabelRSSFeedOpen": "Fuente RSS Abierta", @@ -430,7 +430,7 @@ "LabelSeason": "Temporada", "LabelSelectAllEpisodes": "Seleccionar todos los episodios", "LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles", - "LabelSelectUsers": "Select users", + "LabelSelectUsers": "Seleccionar usuarios", "LabelSendEbookToDevice": "Enviar Ebook a...", "LabelSequence": "Secuencia", "LabelSeries": "Series", @@ -502,14 +502,14 @@ "LabelTagsAccessibleToUser": "Etiquetas Accessibles al Usuario", "LabelTagsNotAccessibleToUser": "Etiquetas no Accesibles al Usuario", "LabelTasks": "Tareas Corriendo", - "LabelTextEditorBulletedList": "Bulleted list", - "LabelTextEditorLink": "Link", - "LabelTextEditorNumberedList": "Numbered list", - "LabelTextEditorUnlink": "Unlink", + "LabelTextEditorBulletedList": "Lista con viñetas", + "LabelTextEditorLink": "Enlazar", + "LabelTextEditorNumberedList": "Lista numerada", + "LabelTextEditorUnlink": "Desenlazar", "LabelTheme": "Tema", "LabelThemeDark": "Oscuro", "LabelThemeLight": "Claro", - "LabelTimeBase": "Time Base", + "LabelTimeBase": "Tiempo Base", "LabelTimeListened": "Tiempo Escuchando", "LabelTimeListenedToday": "Tiempo Escuchando Hoy", "LabelTimeRemaining": "{0} restante", @@ -531,7 +531,7 @@ "LabelTracksSingleTrack": "Una pista", "LabelType": "Tipo", "LabelUnabridged": "No Abreviado", - "LabelUndo": "Undo", + "LabelUndo": "Deshacer", "LabelUnknown": "Desconocido", "LabelUpdateCover": "Actualizar Portada", "LabelUpdateCoverHelp": "Permitir sobrescribir las portadas existentes de los libros seleccionados cuando sean encontradas.", @@ -540,7 +540,7 @@ "LabelUpdateDetailsHelp": "Permitir sobrescribir detalles existentes de los libros seleccionados cuando sean encontrados", "LabelUploaderDragAndDrop": "Arrastre y suelte archivos o carpetas", "LabelUploaderDropFiles": "Suelte los Archivos", - "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", + "LabelUploaderItemFetchMetadataHelp": "Buscar título, autor y series automáticamente", "LabelUseChapterTrack": "Usar pista por capitulo", "LabelUseFullTrack": "Usar pista completa", "LabelUser": "Usuario", @@ -574,15 +574,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?", + "MessageConfirmDeleteLibraryItem": "Esto removerá la librería de la base de datos y archivos en tu sistema. ¿Estás seguro?", + "MessageConfirmDeleteLibraryItems": "Esto removerá {0} elemento(s) de la librería en base de datos y archivos en tu sistema. ¿Estás seguro?", "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?", + "MessageConfirmQuickEmbed": "¡Advertencia! La integración rápida no realiza copias de seguridad a ninguno de tus archivos de audio. Asegúrate de haber realizado una copia de los mismos previamente. <br><br>¿Deseas continuar?", "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}\"?", @@ -597,7 +597,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?", + "MessageConfirmReScanLibraryItems": "¿Estás seguro de querer re escanear {0} elemento(s)?", "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.", @@ -668,7 +668,7 @@ "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.<br /><br />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.<br /><br />Todos los clientes que usen su servidor se actualizarán automáticamente.", "MessageSearchResultsFor": "Resultados de la búsqueda de", - "MessageSelected": "{0} selected", + "MessageSelected": "{0} seleccionado(s)", "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}?", From 682aca0b2a3d8b10189c99cacff06ddf97bcf76e Mon Sep 17 00:00:00 2001 From: Teekeks <info@teawork.de> Date: Thu, 22 Feb 2024 00:36:43 +0100 Subject: [PATCH 0393/2145] feat(i18n): made "Year in Review" UI elements translatable and added german translation for those --- .../components/stats/YearInReviewBanner.vue | 31 +++++++++++-------- client/strings/de.json | 9 ++++++ client/strings/en-us.json | 9 ++++++ 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/client/components/stats/YearInReviewBanner.vue b/client/components/stats/YearInReviewBanner.vue index f7736078..993443b5 100644 --- a/client/components/stats/YearInReviewBanner.vue +++ b/client/components/stats/YearInReviewBanner.vue @@ -7,9 +7,10 @@ </div> <div class="flex items-center"> - <p class="hidden md:block text-xl font-semibold">{{ yearInReviewYear }} Year in Review</p> + <p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p> <div class="hidden md:block flex-grow" /> - <ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? 'Hide Year in Review' : 'See Year in Review' }}</ui-btn> + <ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : + $strings.LabelYearReviewShow }}</ui-btn> </div> <!-- your year in review --> @@ -20,24 +21,27 @@ <!-- previous button --> <ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--"> <span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> - <span class="hidden sm:inline-block pr-2">Previous</span> + <span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span> </ui-btn> <!-- share button --> - <ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview"> Share </ui-btn> + <ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ + $strings.ButtonShare }} + </ui-btn> <div class="flex-grow" /> - <p class="hidden sm:block text-lg font-semibold">Your Year in Review ({{ yearInReviewVariant + 1 }})</p> + <p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }} + </p> <p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p> <div class="flex-grow" /> <!-- refresh button --> <ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview"> - <span class="hidden sm:inline-block">Refresh</span> + <span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span> <span class="material-icons sm:!hidden text-lg py-px">refresh</span> </ui-btn> <!-- next button --> <ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++"> - <span class="hidden sm:inline-block pl-2">Next</span> + <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span> <span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> </ui-btn> </div> @@ -46,7 +50,7 @@ <!-- your year in review short --> <div class="w-full max-w-[800px] mx-auto my-4"> <!-- share button --> - <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort"> Share </ui-btn> + <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn> <stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" /> </div> @@ -56,24 +60,25 @@ <!-- previous button --> <ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--"> <span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> - <span class="hidden sm:inline-block pr-2">Previous</span> + <span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span> </ui-btn> <!-- share button --> - <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer"> Share </ui-btn> + <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} + </ui-btn> <div class="flex-grow" /> - <p class="hidden sm:block text-lg font-semibold">Server Year in Review ({{ yearInReviewServerVariant + 1 }})</p> + <p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p> <p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p> <div class="flex-grow" /> <!-- refresh button --> <ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer"> - <span class="hidden sm:inline-block">Refresh</span> + <span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span> <span class="material-icons sm:!hidden text-lg py-px">refresh</span> </ui-btn> <!-- next button --> <ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++"> - <span class="hidden sm:inline-block pl-2">Next</span> + <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span> <span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> </ui-btn> </div> diff --git a/client/strings/de.json b/client/strings/de.json index 49037f4b..3bfb9189 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -41,12 +41,14 @@ "ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)", "ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)", "ButtonNevermind": "Abbrechen", + "ButtonNext": "Nächste", "ButtonOk": "Ok", "ButtonOpenFeed": "Feed öffnen", "ButtonOpenManager": "Manager öffnen", "ButtonPlay": "Abspielen", "ButtonPlaying": "Spielt", "ButtonPlaylists": "Wiedergabelisten", + "ButtonPrevious": "Vorherige", "ButtonPurgeAllCache": "Cache leeren", "ButtonPurgeItemsCache": "Lösche Medien-Cache", "ButtonPurgeMediaProgress": "Lösche Hörfortschritte", @@ -54,6 +56,7 @@ "ButtonQueueRemoveItem": "Aus der Warteschlange entfernen", "ButtonQuickMatch": "Schnellabgleich", "ButtonRead": "Lesen", + "ButtonRefresh": "Neu Laden", "ButtonRemove": "Löschen", "ButtonRemoveAll": "Alles löschen", "ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge", @@ -73,6 +76,7 @@ "ButtonSelectFolderPath": "Auswahl Ordnerpfad", "ButtonSeries": "Serien", "ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien", + "ButtonShare": "Teilen", "ButtonShiftTimes": "Zeitverschiebung", "ButtonShow": "Anzeigen", "ButtonStartM4BEncode": "M4B-Kodierung starten", @@ -174,6 +178,7 @@ "HeaderUpdateDetails": "Details aktualisieren", "HeaderUpdateLibrary": "Bibliothek aktualisieren", "HeaderUsers": "Benutzer", + "HeaderYearReview": "Jahr {0} in Übersicht", "HeaderYourStats": "Eigene Statistiken", "LabelAbridged": "Gekürzt", "LabelAccountType": "Kontoart", @@ -385,6 +390,7 @@ "LabelPermissionsDownload": "Herunterladen", "LabelPermissionsUpdate": "Aktualisieren", "LabelPermissionsUpload": "Hochladen", + "LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})", "LabelPhotoPathURL": "Foto Pfad/URL", "LabelPlaylists": "Wiedergabelisten", "LabelPlayMethod": "Abspielmethode", @@ -429,6 +435,7 @@ "LabelSeries": "Serien", "LabelSeriesName": "Serienname", "LabelSeriesProgress": "Serienfortschritt", + "LabelServerYearReview": "Server Jahr in Übersicht ({0})", "LabelSetEbookAsPrimary": "Als Hauptbuch setzen", "LabelSetEbookAsSupplementary": "Als Ergänzung setzen", "LabelSettingsAudiobooksOnly": "Nur Hörbücher", @@ -545,6 +552,8 @@ "LabelViewQueue": "Player-Warteschlange anzeigen", "LabelVolume": "Lautstärke", "LabelWeekdaysToRun": "Wochentage für die Ausführung", + "LabelYearReviewHide": "Verstecke Jahr in Übersicht", + "LabelYearReviewShow": "Zeige Jahr in Übersicht", "LabelYourAudiobookDuration": "Laufzeit deines Mediums", "LabelYourBookmarks": "Lesezeichen", "LabelYourPlaylists": "Eigene Wiedergabelisten", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index e3349d1f..4b2d6e4e 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -41,12 +41,14 @@ "ButtonMatchAllAuthors": "Match All Authors", "ButtonMatchBooks": "Match Books", "ButtonNevermind": "Nevermind", + "ButtonNext": "Next", "ButtonOk": "Ok", "ButtonOpenFeed": "Open Feed", "ButtonOpenManager": "Open Manager", "ButtonPlay": "Play", "ButtonPlaying": "Playing", "ButtonPlaylists": "Playlists", + "ButtonPrevious": "Previous", "ButtonPurgeAllCache": "Purge All Cache", "ButtonPurgeItemsCache": "Purge Items Cache", "ButtonPurgeMediaProgress": "Purge Media Progress", @@ -54,6 +56,7 @@ "ButtonQueueRemoveItem": "Remove from queue", "ButtonQuickMatch": "Quick Match", "ButtonRead": "Read", + "ButtonRefresh": "Refresh", "ButtonRemove": "Remove", "ButtonRemoveAll": "Remove All", "ButtonRemoveAllLibraryItems": "Remove All Library Items", @@ -73,6 +76,7 @@ "ButtonSelectFolderPath": "Select Folder Path", "ButtonSeries": "Series", "ButtonSetChaptersFromTracks": "Set chapters from tracks", + "ButtonShare": "Share", "ButtonShiftTimes": "Shift Times", "ButtonShow": "Show", "ButtonStartM4BEncode": "Start M4B Encode", @@ -174,6 +178,7 @@ "HeaderUpdateDetails": "Update Details", "HeaderUpdateLibrary": "Update Library", "HeaderUsers": "Users", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Your Stats", "LabelAbridged": "Abridged", "LabelAccountType": "Account Type", @@ -385,6 +390,7 @@ "LabelPermissionsDownload": "Can Download", "LabelPermissionsUpdate": "Can Update", "LabelPermissionsUpload": "Can Upload", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Photo Path/URL", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Play Method", @@ -429,6 +435,7 @@ "LabelSeries": "Series", "LabelSeriesName": "Series Name", "LabelSeriesProgress": "Series Progress", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSettingsAudiobooksOnly": "Audiobooks only", @@ -545,6 +552,8 @@ "LabelViewQueue": "View player queue", "LabelVolume": "Volume", "LabelWeekdaysToRun": "Weekdays to run", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Your audiobook duration", "LabelYourBookmarks": "Your Bookmarks", "LabelYourPlaylists": "Your Playlists", From 46448ce1e931e0edf79b422be2bf79e5d3a422ac Mon Sep 17 00:00:00 2001 From: rasmuskoit <rasmuskoit@gmail.com> Date: Thu, 22 Feb 2024 09:46:09 +0200 Subject: [PATCH 0394/2145] Adds estonian translation --- client/plugins/i18n.js | 1 + client/strings/et.json | 768 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 769 insertions(+) create mode 100644 client/strings/et.json diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index a4a328c7..021682cf 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -10,6 +10,7 @@ const languageCodeMap = { 'de': { label: 'Deutsch', dateFnsLocale: 'de' }, 'en-us': { label: 'English', dateFnsLocale: 'enUS' }, 'es': { label: 'Español', dateFnsLocale: 'es' }, + 'et': { label: 'Eesti', dateFnsLocale: 'et' }, 'fr': { label: 'Français', dateFnsLocale: 'fr' }, 'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' }, 'it': { label: 'Italiano', dateFnsLocale: 'it' }, diff --git a/client/strings/et.json b/client/strings/et.json new file mode 100644 index 00000000..f0641705 --- /dev/null +++ b/client/strings/et.json @@ -0,0 +1,768 @@ +{ + "ButtonAdd": "Lisa", + "ButtonAddChapters": "Lisa peatükid", + "ButtonAddDevice": "Lisa seade", + "ButtonAddLibrary": "Lisa raamatukogu", + "ButtonAddPodcasts": "Lisa podcastid", + "ButtonAddUser": "Lisa kasutaja", + "ButtonAddYourFirstLibrary": "Lisa oma esimene raamatukogu", + "ButtonApply": "Rakenda", + "ButtonApplyChapters": "Rakenda peatükid", + "ButtonAuthors": "Autorid", + "ButtonBrowseForFolder": "Sirvi kausta", + "ButtonCancel": "Tühista", + "ButtonCancelEncode": "Tühista kodeerimine", + "ButtonChangeRootPassword": "Muuda põhiparooli", + "ButtonCheckAndDownloadNewEpisodes": "Kontrolli ja laadi alla uued episoodid", + "ButtonChooseAFolder": "Vali kaust", + "ButtonChooseFiles": "Vali failid", + "ButtonClearFilter": "Tühista filter", + "ButtonCloseFeed": "Sulge voog", + "ButtonCollections": "Kogud", + "ButtonConfigureScanner": "Konfigureeri skanner", + "ButtonCreate": "Loo", + "ButtonCreateBackup": "Loo varundus", + "ButtonDelete": "Kustuta", + "ButtonDownloadQueue": "Järjekord", + "ButtonEdit": "Muuda", + "ButtonEditChapters": "Muuda peatükke", + "ButtonEditPodcast": "Muuda podcasti", + "ButtonForceReScan": "Sunnitud uuestiskaneerimine", + "ButtonFullPath": "Täielik asukoht", + "ButtonHide": "Peida", + "ButtonHome": "Avaleht", + "ButtonIssues": "Probleemid", + "ButtonJumpBackward": "Hüppa tagasi", + "ButtonJumpForward": "Hüppa edasi", + "ButtonLatest": "Uusim", + "ButtonLibrary": "Raamatukogu", + "ButtonLogout": "Logi välja", + "ButtonLookup": "Otsi", + "ButtonManageTracks": "Halda lugusid", + "ButtonMapChapterTitles": "Kaardista peatükkide pealkirjad", + "ButtonMatchAllAuthors": "Sobita kõik autorid", + "ButtonMatchBooks": "Sobita raamatud", + "ButtonNevermind": "Pole tähtis", + "ButtonNextChapter": "Järgmine peatükk", + "ButtonOk": "Ok", + "ButtonOpenFeed": "Ava voog", + "ButtonOpenManager": "Ava haldur", + "ButtonPause": "Peata", + "ButtonPlay": "Mängi", + "ButtonPlaying": "Mängib", + "ButtonPlaylists": "Esitusloendid", + "ButtonPreviousChapter": "Eelmine peatükk", + "ButtonPurgeAllCache": "Tühjenda kogu vahemälu", + "ButtonPurgeItemsCache": "Tühjenda esemete vahemälu", + "ButtonPurgeMediaProgress": "Tühjenda meedia edenemine", + "ButtonQueueAddItem": "Lisa järjekorda", + "ButtonQueueRemoveItem": "Eemalda järjekorrast", + "ButtonQuickMatch": "Kiire sobitamine", + "ButtonRead": "Loe", + "ButtonRemove": "Eemalda", + "ButtonRemoveAll": "Eemalda kõik", + "ButtonRemoveAllLibraryItems": "Eemalda kõik raamatukogu esemed", + "ButtonRemoveFromContinueListening": "Eemalda jätkake kuulamisest", + "ButtonRemoveFromContinueReading": "Eemalda jätkake lugemisest", + "ButtonRemoveSeriesFromContinueSeries": "Eemalda seeria jätkamisest", + "ButtonReScan": "Uuestiskaneeri", + "ButtonReset": "Lähtesta", + "ButtonResetToDefault": "Lähtesta vaikeseade", + "ButtonRestore": "Taasta", + "ButtonSave": "Salvesta", + "ButtonSaveAndClose": "Salvesta ja sulge", + "ButtonSaveTracklist": "Salvesta lugude loend", + "ButtonScan": "Skanneeri", + "ButtonScanLibrary": "Skanneeri raamatukogu", + "ButtonSearch": "Otsi", + "ButtonSelectFolderPath": "Vali kaustatee", + "ButtonSeries": "Sarjad", + "ButtonSetChaptersFromTracks": "Määra peatükid lugudest", + "ButtonShiftTimes": "Nihke ajad", + "ButtonShow": "Näita", + "ButtonStartM4BEncode": "Alusta M4B kodeerimist", + "ButtonStartMetadataEmbed": "Alusta metaandmete lisamist", + "ButtonSubmit": "Esita", + "ButtonTest": "Test", + "ButtonUpload": "Lae üles", + "ButtonUploadBackup": "Lae üles varundus", + "ButtonUploadCover": "Lae üles ümbris", + "ButtonUploadOPMLFile": "Lae üles OPML-fail", + "ButtonUserDelete": "Kustuta kasutaja {0}", + "ButtonUserEdit": "Muuda kasutajat {0}", + "ButtonViewAll": "Vaata kõiki", + "ButtonYes": "Jah", + "ErrorUploadFetchMetadataAPI": "Viga metaandmete hankimisel", + "ErrorUploadFetchMetadataNoResults": "Ei saanud metaandmeid hankida - proovi tiitlit ja/või autorit uuendada", + "ErrorUploadLacksTitle": "Peab olema pealkiri", + "HeaderAccount": "Konto", + "HeaderAdvanced": "Täpsem", + "HeaderAppriseNotificationSettings": "Apprise teavitamise seaded", + "HeaderAudiobookTools": "Heliraamatu failihaldustööriistad", + "HeaderAudioTracks": "Helirajad", + "HeaderAuthentication": "Autentimine", + "HeaderBackups": "Varukoopiad", + "HeaderChangePassword": "Muuda parooli", + "HeaderChapters": "Peatükid", + "HeaderChooseAFolder": "Vali kaust", + "HeaderCollection": "Kogu", + "HeaderCollectionItems": "Kogu esemed", + "HeaderCover": "Ümbris", + "HeaderCurrentDownloads": "Praegused allalaadimised", + "HeaderCustomMetadataProviders": "Kohandatud metaandmete pakkujad", + "HeaderDetails": "Detailid", + "HeaderDownloadQueue": "Allalaadimise järjekord", + "HeaderEbookFiles": "E-raamatute failid", + "HeaderEmail": "E-post", + "HeaderEmailSettings": "E-posti seaded", + "HeaderEpisodes": "Episoodid", + "HeaderEreaderDevices": "E-lugerite seadmed", + "HeaderEreaderSettings": "E-lugerite seadistused", + "HeaderFiles": "Failid", + "HeaderFindChapters": "Leia peatükid", + "HeaderIgnoredFiles": "Ignoreeritud failid", + "HeaderItemFiles": "Esemete failid", + "HeaderItemMetadataUtils": "Eseme metaandmete tööriistad", + "HeaderLastListeningSession": "Viimane kuulamissessioon", + "HeaderLatestEpisodes": "Viimased episoodid", + "HeaderLibraries": "Raamatukogud", + "HeaderLibraryFiles": "Raamatukogu failid", + "HeaderLibraryStats": "Raamatukogu statistika", + "HeaderListeningSessions": "Kuulamissessioonid", + "HeaderListeningStats": "Kuulamise statistika", + "HeaderLogin": "Logi sisse", + "HeaderLogs": "Logid", + "HeaderManageGenres": "Halda žanre", + "HeaderManageTags": "Halda silte", + "HeaderMapDetails": "Kaardi detailid", + "HeaderMatch": "Sobita", + "HeaderMetadataOrderOfPrecedence": "Metaandmete eelnevusjärjestus", + "HeaderMetadataToEmbed": "Manusta metaandmed", + "HeaderNewAccount": "Uus konto", + "HeaderNewLibrary": "Uus raamatukogu", + "HeaderNotifications": "Teatised", + "HeaderOpenIDConnectAuthentication": "OpenID Connect autentimine", + "HeaderOpenRSSFeed": "Ava RSS-voog", + "HeaderOtherFiles": "Muud failid", + "HeaderPasswordAuthentication": "Parooli autentimine", + "HeaderPermissions": "Õigused", + "HeaderPlayerQueue": "Mängija järjekord", + "HeaderPlaylist": "Mänguloend", + "HeaderPlaylistItems": "Mänguloendi esemed", + "HeaderPodcastsToAdd": "Lisatavad podcastid", + "HeaderPreviewCover": "Eelvaate kaas", + "HeaderRemoveEpisode": "Eemalda episood", + "HeaderRemoveEpisodes": "Eemalda {0} episoodi", + "HeaderRSSFeedGeneral": "RSS-i üksikasjad", + "HeaderRSSFeedIsOpen": "RSS-voog on avatud", + "HeaderRSSFeeds": "RSS-vooged", + "HeaderSavedMediaProgress": "Salvestatud meedia edenemine", + "HeaderSchedule": "Ajakava", + "HeaderScheduleLibraryScans": "Ajasta automaatsed raamatukogu skaneerimised", + "HeaderSession": "Sessioon", + "HeaderSetBackupSchedule": "Määra varunduse ajakava", + "HeaderSettings": "Seaded", + "HeaderSettingsDisplay": "Kuva", + "HeaderSettingsExperimental": "Katsetusfunktsioonid", + "HeaderSettingsGeneral": "Üldised", + "HeaderSettingsScanner": "Skanner", + "HeaderSleepTimer": "Uinaku taimer", + "HeaderStatsLargestItems": "Suurimad esemed", + "HeaderStatsLongestItems": "Kõige pikemad esemed (tunnid)", + "HeaderStatsMinutesListeningChart": "Kuulamise minutid (viimased 7 päeva)", + "HeaderStatsRecentSessions": "Hiljutised sessioonid", + "HeaderStatsTop10Authors": "Top 10 autorit", + "HeaderStatsTop5Genres": "Top 5 žanrit", + "HeaderTableOfContents": "Sisukord", + "HeaderTools": "Tööriistad", + "HeaderUpdateAccount": "Uuenda kontot", + "HeaderUpdateAuthor": "Uuenda autorit", + "HeaderUpdateDetails": "Uuenda detaile", + "HeaderUpdateLibrary": "Uuenda raamatukogu", + "HeaderUsers": "Kasutajad", + "HeaderYourStats": "Sinu statistika", + "LabelAbridged": "Kärbitud", + "LabelAccountType": "Konto tüüp", + "LabelAccountTypeAdmin": "Administraator", + "LabelAccountTypeGuest": "Külaline", + "LabelAccountTypeUser": "Kasutaja", + "LabelActivity": "Tegevus", + "LabelAdded": "Lisatud", + "LabelAddedAt": "Lisatud", + "LabelAddToCollection": "Lisa kogusse", + "LabelAddToCollectionBatch": "Lisa {0} raamatut kogusse", + "LabelAddToPlaylist": "Lisa mänguloendisse", + "LabelAddToPlaylistBatch": "Lisa {0} eset mänguloendisse", + "LabelAdminUsersOnly": "Ainult administraatorid", + "LabelAll": "Kõik", + "LabelAllUsers": "Kõik kasutajad", + "LabelAllUsersExcludingGuests": "Kõik kasutajad, välja arvatud külalised", + "LabelAllUsersIncludingGuests": "Kõik kasutajad, kaasa arvatud külalised", + "LabelAlreadyInYourLibrary": "Juba teie raamatukogus", + "LabelAppend": "Lisa", + "LabelAuthor": "Autor", + "LabelAuthorFirstLast": "Autor (Eesnimi Perekonnanimi)", + "LabelAuthorLastFirst": "Autor (Perekonnanimi, Eesnimi)", + "LabelAuthors": "Autorid", + "LabelAutoDownloadEpisodes": "Automaatne episoodide allalaadimine", + "LabelAutoFetchMetadata": "Automaatne metaandmete hankimine", + "LabelAutoFetchMetadataHelp": "Toob tiitli, autori ja seeria metaandmed üleslaadimise hõlbustamiseks. Lisametaandmed võivad pärast üleslaadimist vajada vastavust.", + "LabelAutoLaunch": "Automaatne käivitamine", + "LabelAutoLaunchDescription": "Suunab automaatselt autentimist pakkuvale teenusele, kui navigeeritakse sisselogimislehele (käsitsi ülekirjutuse tee <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Automaatne registreerimine", + "LabelAutoRegisterDescription": "Loo uued kasutajad automaatselt sisselogimisel", + "LabelBackToUser": "Tagasi kasutajale", + "LabelBackupLocation": "Varukoopia asukoht", + "LabelBackupsEnableAutomaticBackups": "Luba automaatsed varukoopiad", + "LabelBackupsEnableAutomaticBackupsHelp": "Varukoopiad salvestatakse /metadata/backups kausta", + "LabelBackupsMaxBackupSize": "Maksimaalne varukoopia suurus (GB-des)", + "LabelBackupsMaxBackupSizeHelp": "Kaitsena valesti seadistamise vastu ebaõnnestuvad varukoopiad, kui need ületavad seadistatud suuruse.", + "LabelBackupsNumberToKeep": "Varukoopiate arv, mida hoida", + "LabelBackupsNumberToKeepHelp": "Ühel ajal eemaldatakse ainult 1 varukoopia, seega kui teil on juba rohkem varukoopiaid kui siin määratud, peaksite need käsitsi eemaldama.", + "LabelBitrate": "Bittkiirus", + "LabelBooks": "Raamatud", + "LabelButtonText": "Nupu tekst", + "LabelChangePassword": "Muuda parooli", + "LabelChannels": "Kanalid", + "LabelChapters": "Peatükid", + "LabelChaptersFound": "peatükid leitud", + "LabelChapterTitle": "Peatüki pealkiri", + "LabelClickForMoreInfo": "Klõpsa lisateabe saamiseks", + "LabelClosePlayer": "Sulge mängija", + "LabelCodec": "Kodek", + "LabelCollapseSeries": "Ahenda seeria", + "LabelCollection": "Kogu", + "LabelCollections": "Kogud", + "LabelComplete": "Valmis", + "LabelConfirmPassword": "Kinnita parool", + "LabelContinueListening": "Jätka kuulamist", + "LabelContinueReading": "Jätka lugemist", + "LabelContinueSeries": "Jätka seeriat", + "LabelCover": "Ümbris", + "LabelCoverImageURL": "Ümbrise pildi URL", + "LabelCreatedAt": "Loodud", + "LabelCronExpression": "Croni valem", + "LabelCurrent": "Praegune", + "LabelCurrently": "Praegu:", + "LabelCustomCronExpression": "Kohandatud Croni valem:", + "LabelDatetime": "Kuupäev ja kellaaeg", + "LabelDeleteFromFileSystemCheckbox": "Kustuta failisüsteemist (ärge märkige seda ära, et eemaldada ainult andmebaasist)", + "LabelDescription": "Kirjeldus", + "LabelDeselectAll": "Tühista kõigi valimine", + "LabelDevice": "Seade", + "LabelDeviceInfo": "Seadme info", + "LabelDeviceIsAvailableTo": "Seade on saadaval kasutajale...", + "LabelDirectory": "Kataloog", + "LabelDiscFromFilename": "Ketas failinimest", + "LabelDiscFromMetadata": "Ketas metaandmetest", + "LabelDiscover": "Avasta", + "LabelDownload": "Lae alla", + "LabelDownloadNEpisodes": "Lae alla {0} episoodi", + "LabelDuration": "Kestus", + "LabelDurationFound": "Leitud kestus:", + "LabelEbook": "E-raamat", + "LabelEbooks": "E-raamatud", + "LabelEdit": "Muuda", + "LabelEmail": "E-post", + "LabelEmailSettingsFromAddress": "Saatja aadress", + "LabelEmailSettingsSecure": "Turvaline", + "LabelEmailSettingsSecureHelp": "Kui see on tõene, kasutab ühendus serveriga ühenduse loomisel TLS-i. Kui see on väär, kasutatakse TLS-i, kui server toetab STARTTLS-i laiendust. Enamikul juhtudest seadke see väärtus tõeks, kui ühendate pordile 465. Pordi 587 või 25 korral hoidke seda väär. (nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Testi aadress", + "LabelEmbeddedCover": "Manustatud kaas", + "LabelEnable": "Luba", + "LabelEnd": "Lõpp", + "LabelEpisode": "Episood", + "LabelEpisodeTitle": "Episoodi pealkiri", + "LabelEpisodeType": "Episoodi tüüp", + "LabelExample": "Näide", + "LabelExplicit": "Vulgaarne", + "LabelFeedURL": "Voogu URL", + "LabelFetchingMetadata": "Metaandmete hankimine", + "LabelFile": "Fail", + "LabelFileBirthtime": "Faili sünniaeg", + "LabelFileModified": "Faili muudetud", + "LabelFilename": "Failinimi", + "LabelFilterByUser": "Filtri alusel kasutaja järgi", + "LabelFindEpisodes": "Otsi episoodid", + "LabelFinished": "Lõpetatud", + "LabelFolder": "Kaust", + "LabelFolders": "Kataloogid", + "LabelFontBold": "Paks", + "LabelFontFamily": "Fondi pere", + "LabelFontItalic": "Kaldkiri", + "LabelFontScale": "Fondi suurus", + "LabelFontStrikethrough": "Üle joonitud", + "LabelFormat": "Vorming", + "LabelGenre": "Žanr", + "LabelGenres": "Žanrid", + "LabelHardDeleteFile": "Faili lõplik kustutamine", + "LabelHasEbook": "On e-raamat", + "LabelHasSupplementaryEbook": "On täiendav e-raamat", + "LabelHighestPriority": "Kõrgeim prioriteet", + "LabelHost": "Host", + "LabelHour": "Tund", + "LabelIcon": "Ikoon", + "LabelImageURLFromTheWeb": "Pildi URL veebist", + "LabelIncludeInTracklist": "Kaasa jälgimisloendis", + "LabelIncomplete": "Puudulik", + "LabelInProgress": "Pooleli", + "LabelInterval": "Intervall", + "LabelIntervalCustomDailyWeekly": "Kohandatud päevane/nädalane", + "LabelIntervalEvery12Hours": "Iga 12 tunni tagant", + "LabelIntervalEvery15Minutes": "Iga 15 minuti tagant", + "LabelIntervalEvery2Hours": "Iga 2 tunni tagant", + "LabelIntervalEvery30Minutes": "Iga 30 minuti tagant", + "LabelIntervalEvery6Hours": "Iga 6 tunni tagant", + "LabelIntervalEveryDay": "Iga päev", + "LabelIntervalEveryHour": "Iga tunni tagant", + "LabelInvalidParts": "Vigased osad", + "LabelInvert": "Pööra ümber", + "LabelItem": "Kirje", + "LabelLanguage": "Keel", + "LabelLanguageDefaultServer": "Vaikeserveri keel", + "LabelLastBookAdded": "Viimati lisatud raamat", + "LabelLastBookUpdated": "Viimati uuendatud raamat", + "LabelLastSeen": "Viimati nähtud", + "LabelLastTime": "Viimati aeg", + "LabelLastUpdate": "Viimane uuendus", + "LabelLayout": "Paigutus", + "LabelLayoutSinglePage": "Üks lehekülg", + "LabelLayoutSplitPage": "Jagatud lehekülg", + "LabelLess": "Vähem", + "LabelLibrariesAccessibleToUser": "Kasutajale ligipääsetavad raamatukogud", + "LabelLibrary": "Raamatukogu", + "LabelLibraryItem": "Raamatukogu kirje", + "LabelLibraryName": "Raamatukogu nimi", + "LabelLimit": "Piirang", + "LabelLineSpacing": "Joonevahe", + "LabelListenAgain": "Kuula uuesti", + "LabelLogLevelDebug": "Silumine", + "LabelLogLevelInfo": "Teave", + "LabelLogLevelWarn": "Hoiatus", + "LabelLookForNewEpisodesAfterDate": "Otsi uusi episoodid pärast seda kuupäeva", + "LabelLowestPriority": "Madalaim prioriteet", + "LabelMatchExistingUsersBy": "Sobita olemasolevad kasutajad", + "LabelMatchExistingUsersByDescription": "Kasutatakse olemasolevate kasutajate ühendamiseks. Ühendatud kasutajaid sobitatakse teie SSO pakkuja unikaalse ID järgi.", + "LabelMediaPlayer": "Meediapleier", + "LabelMediaType": "Meedia tüüp", + "LabelMetadataOrderOfPrecedenceDescription": "Kõrgema prioriteediga metaandmete allikad võtavad üle madalama prioriteediga metaandmete allikad", + "LabelMetadataProvider": "Metaandmete pakkuja", + "LabelMetaTag": "Meta märge", + "LabelMetaTags": "Meta märgendid", + "LabelMinute": "Minut", + "LabelMissing": "Puudub", + "LabelMissingParts": "Puuduvad osad", + "LabelMobileRedirectURIs": "Lubatud mobiilile suunamise URI-d", + "LabelMobileRedirectURIsDescription": "See on mobiilirakenduste jaoks kehtivate suunamise URI-de lubatud nimekiri. Vaikimisi on selleks <code>audiobookshelf://oauth</code>, mida saate eemaldada või täiendada täiendavate URI-dega kolmanda osapoole rakenduste integreerimiseks. Tärni (<code>*</code>) ainukese kirjena kasutamine võimaldab mis tahes URI-d.", + "LabelMore": "Rohkem", + "LabelMoreInfo": "Rohkem infot", + "LabelName": "Nimi", + "LabelNarrator": "Jutustaja", + "LabelNarrators": "Jutustajad", + "LabelNew": "Uus", + "LabelNewestAuthors": "Uusimad autorid", + "LabelNewestEpisodes": "Uusimad episoodid", + "LabelNewPassword": "Uus parool", + "LabelNextBackupDate": "Järgmine varukoopia kuupäev", + "LabelNextScheduledRun": "Järgmine ajakava järgmine", + "LabelNoEpisodesSelected": "Episoodid pole valitud", + "LabelNotes": "Märkused", + "LabelNotFinished": "Ei ole lõpetatud", + "LabelNotificationAppriseURL": "Apprise URL-id", + "LabelNotificationAvailableVariables": "Saadaolevad muutujad", + "LabelNotificationBodyTemplate": "Keha mall", + "LabelNotificationEvent": "Teavituse sündmus", + "LabelNotificationsMaxFailedAttempts": "Maksimaalsed ebaõnnestunud katsed", + "LabelNotificationsMaxFailedAttemptsHelp": "Teatised keelatakse, kui need ebaõnnestuvad nii palju kordi", + "LabelNotificationsMaxQueueSize": "Teavituste sündmuste maksimaalne järjekorra suurus", + "LabelNotificationsMaxQueueSizeHelp": "Sündmused on piiratud 1 sekundiga. Sündmusi ignoreeritakse, kui järjekord on maksimumsuuruses. See takistab teavituste rämpsposti.", + "LabelNotificationTitleTemplate": "Pealkirja mall", + "LabelNotStarted": "Pole alustatud", + "LabelNumberOfBooks": "Raamatute arv", + "LabelNumberOfEpisodes": "Episoodide arv", + "LabelOpenRSSFeed": "Ava RSS voog", + "LabelOverwrite": "Kirjuta üle", + "LabelPassword": "Parool", + "LabelPath": "Asukoht", + "LabelPermissionsAccessAllLibraries": "Saab ligi kõikidele raamatukogudele", + "LabelPermissionsAccessAllTags": "Saab ligi kõikidele siltidele", + "LabelPermissionsAccessExplicitContent": "Saab ligi vulgaarsele sisule", + "LabelPermissionsDelete": "Saab kustutada", + "LabelPermissionsDownload": "Saab alla laadida", + "LabelPermissionsUpdate": "Saab uuendada", + "LabelPermissionsUpload": "Saab üles laadida", + "LabelPhotoPathURL": "Foto tee/URL", + "LabelPlaylists": "Mänguloendid", + "LabelPlayMethod": "Esitusmeetod", + "LabelPodcast": "Podcast", + "LabelPodcasts": "Podcastid", + "LabelPodcastSearchRegion": "Podcasti otsingu piirkond", + "LabelPodcastType": "Podcasti tüüp", + "LabelPort": "Port", + "LabelPrefixesToIgnore": "Eiramiseks eesliited (tõstutundetu)", + "LabelPreventIndexing": "Vältige oma voogu indekseerimist iTunes'i ja Google podcasti kataloogides", + "LabelPrimaryEbook": "Esmane e-raamat", + "LabelProgress": "Edenemine", + "LabelProvider": "Pakkuja", + "LabelPubDate": "Avaldamise kuupäev", + "LabelPublisher": "Kirjastaja", + "LabelPublishYear": "Aasta avaldamine", + "LabelRead": "Lugenud", + "LabelReadAgain": "Loe uuesti", + "LabelReadEbookWithoutProgress": "Lugege e-raamatut ilma edenemist säilitamata", + "LabelRecentlyAdded": "Hiljuti lisatud", + "LabelRecentSeries": "Hiljutised seeriad", + "LabelRecommended": "Soovitatud", + "LabelRedo": "Tee uuesti", + "LabelRegion": "Piirkond", + "LabelReleaseDate": "Väljalaske kuupäev", + "LabelRemoveCover": "Eemalda ümbris", + "LabelRowsPerPage": "Rida lehe kohta", + "LabelRSSFeedCustomOwnerEmail": "Kohandatud omaniku e-post", + "LabelRSSFeedCustomOwnerName": "Kohandatud omaniku nimi", + "LabelRSSFeedOpen": "Ava RSS voog", + "LabelRSSFeedPreventIndexing": "Vältige indekseerimist", + "LabelRSSFeedSlug": "RSS voog Slug", + "LabelRSSFeedURL": "RSS voog URL", + "LabelSearchTerm": "Otsingutermin", + "LabelSearchTitle": "Otsi pealkirja", + "LabelSearchTitleOrASIN": "Otsi pealkirja või ASIN-i", + "LabelSeason": "Hooaeg", + "LabelSelectAllEpisodes": "Vali kõik episoodid", + "LabelSelectEpisodesShowing": "Valige {0} näidatavat episoodi", + "LabelSelectUsers": "Valige kasutajad", + "LabelSendEbookToDevice": "Saada e-raamat seadmele...", + "LabelSequence": "Järjestus", + "LabelSeries": "Seeria", + "LabelSeriesName": "Seeria nimi", + "LabelSeriesProgress": "Seeria edenemine", + "LabelSetEbookAsPrimary": "Määra peamiseks", + "LabelSetEbookAsSupplementary": "Määra täiendavaks", + "LabelSettingsAudiobooksOnly": "Ainult heliraamatud", + "LabelSettingsAudiobooksOnlyHelp": "Selle seadistuse lubamine eirab e-raamatute faile, välja arvatud juhul, kui need on heliraamatu kaustas, kus need seatakse täiendavate e-raamatutena", + "LabelSettingsBookshelfViewHelp": "Skeumorfne kujundus puidust riiulitega", + "LabelSettingsChromecastSupport": "Chromecasti tugi", + "LabelSettingsDateFormat": "Kuupäeva vorming", + "LabelSettingsDisableWatcher": "Keela vaatamine", + "LabelSettingsDisableWatcherForLibrary": "Keela kaustavaatamine raamatukogu jaoks", + "LabelSettingsDisableWatcherHelp": "Keelab automaatse lisamise/uuendamise, kui failimuudatusi tuvastatakse. *Nõuab serveri taaskäivitamist", + "LabelSettingsEnableWatcher": "Luba vaatamine", + "LabelSettingsEnableWatcherForLibrary": "Luba kaustavaatamine raamatukogu jaoks", + "LabelSettingsEnableWatcherHelp": "Lubab automaatset lisamist/uuendamist, kui tuvastatakse failimuudatused. *Nõuab serveri taaskäivitamist", + "LabelSettingsExperimentalFeatures": "Eksperimentaalsed funktsioonid", + "LabelSettingsExperimentalFeaturesHelp": "Arengus olevad funktsioonid, mis vajavad teie tagasisidet ja abi testimisel. Klõpsake GitHubi arutelu avamiseks.", + "LabelSettingsFindCovers": "Leia ümbrised", + "LabelSettingsFindCoversHelp": "Kui teie heliraamatul pole sisseehitatud ümbrist ega ümbrise pilti kaustas, proovib skanner leida ümbrist.<br>Märkus: see pikendab skaneerimisaega", + "LabelSettingsHideSingleBookSeries": "Peida üksikute raamatute seeriad", + "LabelSettingsHideSingleBookSeriesHelp": "Ühe raamatuga seeriaid peidetakse seeria lehelt ja avalehe riiulitelt.", + "LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet", + "LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet", + "LabelSettingsParseSubtitles": "Lugege subtiitreid", + "LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest.<br>Subtiitrid peavad olema eraldatud \" - \".<br>Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"", + "LabelSettingsPreferMatchedMetadata": "Eelista sobitatud metaandmeid", + "LabelSettingsPreferMatchedMetadataHelp": "Sobitatud andmed kirjutavad Kiir Sobitamise kasutamisel üle üksikasjad.", + "LabelSettingsSkipMatchingBooksWithASIN": "Jätke ASIN-iga sobituvad raamatud vahele", + "LabelSettingsSkipMatchingBooksWithISBN": "Jätke ISBN-iga sobituvad raamatud vahele", + "LabelSettingsSortingIgnorePrefixes": "Ignoreeri eesliiteid sortimisel", + "LabelSettingsSortingIgnorePrefixesHelp": "nt. eesliidet \"the\" kasutades raamatu pealkiri \"The Book Title\" sorteeritakse \"Book Title, The\"", + "LabelSettingsSquareBookCovers": "Kasutage ruudukujulisi raamatu kaasi", + "LabelSettingsSquareBookCoversHelp": "Eelistage ruudukujulisi kaasi tavaliste 1.6:1 raamatu ümbrise asemel", + "LabelSettingsStoreCoversWithItem": "Salvesta kaaned üksusega", + "LabelSettingsStoreCoversWithItemHelp": "Vaikimisi salvestatakse kaaned /metadata/items kausta. Selle seadistuse lubamine salvestab kaaned teie raamatukogu üksuse kausta. Hoitakse ainult ühte faili nimega \"kaas\"", + "LabelSettingsStoreMetadataWithItem": "Salvesta metaandmed üksusega", + "LabelSettingsStoreMetadataWithItemHelp": "Vaikimisi salvestatakse metaandmed /metadata/items kausta. Selle seadistuse lubamine salvestab metaandmed teie raamatukogu üksuse kaustadesse", + "LabelSettingsTimeFormat": "Kellaaja vorming", + "LabelShowAll": "Näita kõiki", + "LabelSize": "Suurus", + "LabelSleepTimer": "Uinaku taimer", + "LabelSlug": "Slug", + "LabelStart": "Alusta", + "LabelStarted": "Alustatud", + "LabelStartedAt": "Alustatud", + "LabelStartTime": "Alustamise aeg", + "LabelStatsAudioTracks": "Audiojäljed", + "LabelStatsAuthors": "Autorid", + "LabelStatsBestDay": "Parim päev", + "LabelStatsDailyAverage": "Päevane keskmine", + "LabelStatsDays": "Päevad", + "LabelStatsDaysListened": "Kuulatud päevad", + "LabelStatsHours": "Tunnid", + "LabelStatsInARow": "järjest", + "LabelStatsItemsFinished": "Lõpetatud üksused", + "LabelStatsItemsInLibrary": "Üksused raamatukogus", + "LabelStatsMinutes": "minutit", + "LabelStatsMinutesListening": "Kuulamise minutid", + "LabelStatsOverallDays": "Kokku päevad", + "LabelStatsOverallHours": "Kokku tunnid", + "LabelStatsWeekListening": "Nädala kuulamine", + "LabelSubtitle": "Alapealkiri", + "LabelSupportedFileTypes": "Toetatud failitüübid", + "LabelTag": "Silt", + "LabelTags": "Sildid", + "LabelTagsAccessibleToUser": "Kasutajale kättesaadavad sildid", + "LabelTagsNotAccessibleToUser": "Kasutajale mittekättesaadavad sildid", + "LabelTasks": "Käimasolevad ülesanded", + "LabelTextEditorBulletedList": "Punktloend", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "Numberloend", + "LabelTextEditorUnlink": "Eemalda link", + "LabelTheme": "Teema", + "LabelThemeDark": "Tume", + "LabelThemeLight": "Hele", + "LabelTimeBase": "Aja alus", + "LabelTimeListened": "Kuulatud aeg", + "LabelTimeListenedToday": "Täna kuulatud aeg", + "LabelTimeRemaining": "{0} jäänud", + "LabelTimeToShift": "Nihutamiseks sekundites kuluv aeg", + "LabelTitle": "Pealkiri", + "LabelToolsEmbedMetadata": "Manusta metaandmed", + "LabelToolsEmbedMetadataDescription": "Manusta metaandmed helifailidesse, sealhulgas kaanepilt ja peatükid.", + "LabelToolsMakeM4b": "Loo M4B heliraamatu fail", + "LabelToolsMakeM4bDescription": "Loo .M4B heliraamatu fail, kuhu on manustatud metaandmed, kaanepilt ja peatükid.", + "LabelToolsSplitM4b": "Jaga M4B MP3-deks", + "LabelToolsSplitM4bDescription": "Loo MP3-d M4B-st peatükkide kaupa, kus on manustatud metaandmed, kaanepilt ja peatükid.", + "LabelTotalDuration": "Kogukestus", + "LabelTotalTimeListened": "Kogu kuulatud aeg", + "LabelTrackFromFilename": "Jälg nimest", + "LabelTrackFromMetadata": "Jälg metaandmetest", + "LabelTracks": "Jäljed", + "LabelTracksMultiTrack": "Mitmejälg", + "LabelTracksNone": "Ühtegi jälgimist", + "LabelTracksSingleTrack": "Üksikjälg", + "LabelType": "Tüüp", + "LabelUnabridged": "Täismahus", + "LabelUndo": "Võta tagasi", + "LabelUnknown": "Tundmatu", + "LabelUpdateCover": "Uuenda kaant", + "LabelUpdateCoverHelp": "Luba üle kirjutamine olemasolevate kaante jaoks valitud raamatutele, kui leitakse sobivus", + "LabelUpdatedAt": "Uuendatud", + "LabelUpdateDetails": "Uuenda üksikasju", + "LabelUpdateDetailsHelp": "Luba üle kirjutamine olemasolevate üksikasjade jaoks valitud raamatutele, kui leitakse sobivus", + "LabelUploaderDragAndDrop": "Lohista ja aseta faile või kaustu", + "LabelUploaderDropFiles": "Aseta failid", + "LabelUploaderItemFetchMetadataHelp": "Hangi automaatselt pealkiri, autor ja seeria", + "LabelUseChapterTrack": "Kasuta peatüki jälge", + "LabelUseFullTrack": "Kasuta täielikku jälge", + "LabelUser": "Kasutaja", + "LabelUsername": "Kasutajanimi", + "LabelValue": "Väärtus", + "LabelVersion": "Versioon", + "LabelViewBookmarks": "Vaata järjehoidjaid", + "LabelViewChapters": "Vaata peatükke", + "LabelViewQueue": "Vaata esitusjärjekorda", + "LabelVolume": "Heli tugevus", + "LabelWeekdaysToRun": "Päevad nädalas käivitamiseks", + "LabelYourAudiobookDuration": "Teie heliraamatu kestus", + "LabelYourBookmarks": "Teie järjehoidjad", + "LabelYourPlaylists": "Teie esitusloendid", + "LabelYourProgress": "Teie edenemine", + "MessageAddToPlayerQueue": "Lisa esitusjärjekorda", + "MessageAppriseDescription": "Selle funktsiooni kasutamiseks peate käivitama <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> eksemplari või API, mis töötleb samu päringuid. <br />Apprise API URL peaks olema täielik URL-rada teatise saatmiseks, näiteks kui teie API eksemplar töötab aadressil <code>http://192.168.1.1:8337</code>, siis peaksite sisestama <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "Varukoopiad hõlmavad kasutajaid, kasutajate edenemist, raamatukogu üksikasju, serveri seadeid ja kaustades <code>/metadata/items</code> ja <code>/metadata/authors</code> salvestatud pilte. Varukoopiad ei hõlma ühtegi teie raamatukogu kaustades olevat faili.", + "MessageBatchQuickMatchDescription": "Kiire sobitamine üritab lisada valitud üksustele puuduvad kaaned ja metaandmed. Luba allpool olevad valikud, et lubada Kiire sobitamine'il üle kirjutada olemasolevaid kaasi ja/või metaandmeid.", + "MessageBookshelfNoCollections": "Te pole veel ühtegi kogumit teinud", + "MessageBookshelfNoResultsForFilter": "Filtrile \"{0}: {1}\" pole tulemusi", + "MessageBookshelfNoRSSFeeds": "Ühtegi RSS-i voogu pole avatud", + "MessageBookshelfNoSeries": "Teil pole ühtegi seeriat", + "MessageChapterEndIsAfter": "Peatüki lõpp on pärast teie heliraamatu lõppu", + "MessageChapterErrorFirstNotZero": "Esimene peatükk peab algama 0-st", + "MessageChapterErrorStartGteDuration": "Vigane algusaeg peab olema väiksem kui heliraamatu kestus", + "MessageChapterErrorStartLtPrev": "Vigane algusaeg peab olema suurem või võrdne eelneva peatüki algusajaga", + "MessageChapterStartIsAfter": "Peatüki algus on pärast teie heliraamatu lõppu", + "MessageCheckingCron": "Croni kontrollimine...", + "MessageConfirmCloseFeed": "Olete kindel, et soovite selle voo sulgeda?", + "MessageConfirmDeleteBackup": "Olete kindel, et soovite varukoopia kustutada {0} kohta?", + "MessageConfirmDeleteFile": "See kustutab faili teie failisüsteemist. Olete kindel?", + "MessageConfirmDeleteLibrary": "Olete kindel, et soovite raamatukogu \"{0}\" lõplikult kustutada?", + "MessageConfirmDeleteLibraryItem": "See kustutab raamatukogu üksuse andmebaasist ja failisüsteemist. Olete kindel?", + "MessageConfirmDeleteLibraryItems": "See kustutab {0} raamatukogu üksust andmebaasist ja failisüsteemist. Olete kindel?", + "MessageConfirmDeleteSession": "Olete kindel, et soovite selle seansi kustutada?", + "MessageConfirmForceReScan": "Olete kindel, et soovite jõuga uuesti skannida?", + "MessageConfirmMarkAllEpisodesFinished": "Olete kindel, et soovite kõik episoodid lõpetatuks märkida?", + "MessageConfirmMarkAllEpisodesNotFinished": "Olete kindel, et soovite kõik episoodid mitte lõpetatuks märkida?", + "MessageConfirmMarkSeriesFinished": "Olete kindel, et soovite selle seeria kõik raamatud lõpetatuks märkida?", + "MessageConfirmMarkSeriesNotFinished": "Olete kindel, et soovite selle seeria kõik raamatud mitte lõpetatuks märkida?", + "MessageConfirmQuickEmbed": "Hoiatus! Quick Embed ei tee varukoopiaid teie helifailidest. Veenduge, et teil oleks varukoopia oma helifailidest. <br><br>Kas soovite jätkata?", + "MessageConfirmRemoveAllChapters": "Olete kindel, et soovite eemaldada kõik peatükid?", + "MessageConfirmRemoveAuthor": "Olete kindel, et soovite autori \"{0}\" eemaldada?", + "MessageConfirmRemoveCollection": "Olete kindel, et soovite kogumi \"{0}\" eemaldada?", + "MessageConfirmRemoveEpisode": "Olete kindel, et soovite episoodi \"{0}\" eemaldada?", + "MessageConfirmRemoveEpisodes": "Olete kindel, et soovite eemaldada {0} episoodi?", + "MessageConfirmRemoveListeningSessions": "Olete kindel, et soovite eemaldada {0} kuulamise sessiooni?", + "MessageConfirmRemoveNarrator": "Olete kindel, et soovite jutustaja \"{0}\" eemaldada?", + "MessageConfirmRemovePlaylist": "Olete kindel, et soovite eemaldada oma esitusloendi \"{0}\"?", + "MessageConfirmRenameGenre": "Olete kindel, et soovite žanri \"{0}\" ümber nimetada kujule \"{1}\" kõikidele üksustele?", + "MessageConfirmRenameGenreMergeNote": "Märkus: See žanr on juba olemas, nii et need ühendatakse.", + "MessageConfirmRenameGenreWarning": "Hoiatus! Sarnane žanr erineva puhvriga on juba olemas \"{0}\".", + "MessageConfirmRenameTag": "Olete kindel, et soovite silti \"{0}\" ümber nimetada kujule \"{1}\" kõikidele üksustele?", + "MessageConfirmRenameTagMergeNote": "Märkus: See silt on juba olemas, nii et need ühendatakse.", + "MessageConfirmRenameTagWarning": "Hoiatus! Sarnane silt erineva puhvriga on juba olemas \"{0}\".", + "MessageConfirmReScanLibraryItems": "Olete kindel, et soovite uuesti skannida {0} üksust?", + "MessageConfirmSendEbookToDevice": "Olete kindel, et soovite saata {0} e-raamatu \"{1}\" seadmesse \"{2}\"?", + "MessageDownloadingEpisode": "Episoodi allalaadimine", + "MessageDragFilesIntoTrackOrder": "Lohistage failid õigesse järjekorda", + "MessageEmbedFinished": "Manustamine lõpetatud!", + "MessageEpisodesQueuedForDownload": "{0} Episood(i) on allalaadimiseks järjekorras", + "MessageFeedURLWillBe": "Toite URL saab olema {0}", + "MessageFetching": "Hangitakse...", + "MessageForceReScanDescription": "skaneerib kõik failid uuesti nagu värsket skannimist. Heli faili ID3 silte, OPF faile ja tekstifaile skaneeritakse uuesti.", + "MessageImportantNotice": "Oluline märkus!", + "MessageInsertChapterBelow": "Sisesta peatükk allapoole", + "MessageItemsSelected": "{0} Valitud üksust", + "MessageItemsUpdated": "{0} Üksust on uuendatud", + "MessageJoinUsOn": "Liitu meiega", + "MessageListeningSessionsInTheLastYear": "Kuulamissessioone viimase aasta jooksul: {0}", + "MessageLoading": "Laadimine...", + "MessageLoadingFolders": "Kaustade laadimine...", + "MessageM4BFailed": "M4B ebaõnnestus!", + "MessageM4BFinished": "M4B lõpetatud!", + "MessageMapChapterTitles": "Kaarda peatükkide pealkirjad olemasolevatele heliraamatu peatükkidele, ajatempe ei muudeta", + "MessageMarkAllEpisodesFinished": "Märgi kõik episoodid lõpetatuks", + "MessageMarkAllEpisodesNotFinished": "Märgi kõik episoodid mitte lõpetatuks", + "MessageMarkAsFinished": "Märgi lõpetatuks", + "MessageMarkAsNotFinished": "Märgi mitte lõpetatuks", + "MessageMatchBooksDescription": "üritab raamatuid raamatukogus sobitada otsingupakkujast leitud raamatuga ning täita tühjad üksikasjad ja kaas. Ei üle kirjuta üksikasju.", + "MessageNoAudioTracks": "Ühtegi helijälge pole", + "MessageNoAuthors": "Ühtegi autori pole", + "MessageNoBackups": "Ühtegi varukoopia pole", + "MessageNoBookmarks": "Ühtegi järjehoidjat pole", + "MessageNoChapters": "Ühtegi peatükki pole", + "MessageNoCollections": "Ühtegi kogumit pole", + "MessageNoCoversFound": "Ühtegi kaant pole leitud", + "MessageNoDescription": "Kirjeldust pole", + "MessageNoDownloadsInProgress": "Praegu allalaadimisi pole", + "MessageNoDownloadsQueued": "Pole järjekorras allalaadimisi", + "MessageNoEpisodeMatchesFound": "Ühtegi episoodi vastet pole leitud", + "MessageNoEpisodes": "Ühtegi episoodi pole", + "MessageNoFoldersAvailable": "Ühtegi kausta pole saadaval", + "MessageNoGenres": "Ühtegi žanrit pole", + "MessageNoIssues": "Ühtegi probleemi pole", + "MessageNoItems": "Ühtegi üksust pole", + "MessageNoItemsFound": "Ühtegi üksust pole leitud", + "MessageNoListeningSessions": "Ühtegi kuulamissessiooni pole", + "MessageNoLogs": "Ühtegi logi pole", + "MessageNoMediaProgress": "Ühtegi meediaprogressi pole", + "MessageNoNotifications": "Ühtegi teavitust pole", + "MessageNoPodcastsFound": "Ühtegi podcasti pole leitud", + "MessageNoResults": "Ühtegi tulemust pole", + "MessageNoSearchResultsFor": "Otsingutulemusi pole märksõna kohta: \"{0}\"", + "MessageNoSeries": "Ühtegi seeriat pole", + "MessageNoTags": "Ühtegi silti pole", + "MessageNoTasksRunning": "Ühtegi käimasolevat ülesannet pole", + "MessageNotYetImplemented": "Pole veel ellu viidud", + "MessageNoUpdateNecessary": "Ühtegi värskendust pole vaja", + "MessageNoUpdatesWereNecessary": "Ühtegi värskendust polnud vaja", + "MessageNoUserPlaylists": "Teil pole ühtegi esitusloendit", + "MessageOr": "või", + "MessagePauseChapter": "Peata peatüki esitamine", + "MessagePlayChapter": "Kuula peatüki algust", + "MessagePlaylistCreateFromCollection": "Loo esitusloend kogumist", + "MessagePodcastHasNoRSSFeedForMatching": "Podcastil pole sobitamiseks RSS-voogu", + "MessageQuickMatchDescription": "täidab tühjad üksikasjad ja kaaned raamatukogus esimese otsingutulemusega rakendusest '{0}'. Ei üle kirjuta üksikasju, välja arvatud juhul, kui serveri sätetes on lubatud 'Eelista sobitatud metaandmeid'.", + "MessageRemoveChapter": "Eemalda peatükk", + "MessageRemoveEpisodes": "Eemalda {0} episood(i)", + "MessageRemoveFromPlayerQueue": "Eemalda esitusjärjekorrast", + "MessageRemoveUserWarning": "Olete kindel, et soovite kasutaja \"{0}\" lõplikult kustutada?", + "MessageReportBugsAndContribute": "Raporteeri vigu, palu funktsioone ja aita kaasa", + "MessageResetChaptersConfirm": "Olete kindel, et soovite peatükkide lähtestada ja tehtud muudatused tagasi võtta?", + "MessageRestoreBackupConfirm": "Olete kindel, et soovite taastada varukoopia, mis loodi", + "MessageRestoreBackupWarning": "Varukoopia taastamine kirjutab üle kogu /config ja /metadata/items & /metadata/authors kaustas oleva andmebaasi. <br /><br />Varukoopiad ei muuda teie raamatukogukaustades olevaid faile. Kui olete lubanud serveri sätetel salvestada kaane kunsti ja metaandmed teie raamatukogu kaustadesse, siis neid ei varundata ega kirjutata üle.<br /><br />Kõik teie serveri kasutavad kliendid värskendatakse automaatselt.", + "MessageSearchResultsFor": "Otsingutulemused märksõnale", + "MessageSelected": "{0} valitud", + "MessageServerCouldNotBeReached": "Serveriga ei saanud ühendust luua", + "MessageSetChaptersFromTracksDescription": "Määrake peatükid, kasutades iga helifaili peatükina ja peatüki pealkirjana helifaili nime", + "MessageStartPlaybackAtTime": "Alustage \"{0}\" esitamist kell {1}?", + "MessageThinking": "Mõtlen...", + "MessageUploaderItemFailed": "Üleslaadimine ebaõnnestus", + "MessageUploaderItemSuccess": "Edukalt üles laaditud!", + "MessageUploading": "Üles laadimine...", + "MessageValidCronExpression": "Kehtiv cron-väljend", + "MessageWatcherIsDisabledGlobally": "Vaatleja on ülemaailmselt keelatud serveri sätetes", + "MessageXLibraryIsEmpty": "{0} raamatukogu on tühi!", + "MessageYourAudiobookDurationIsLonger": "Teie heliraamatu kestus on pikem kui leitud kestus", + "MessageYourAudiobookDurationIsShorter": "Teie heliraamatu kestus on lühem kui leitud kestus", + "NoteChangeRootPassword": "Root kasutajal võib olla ainus kasutaja, kellel võib olla tühi parool", + "NoteChapterEditorTimes": "Märkus: Esimese peatüki algusaeg peab jääma 0:00 ja viimase peatüki algusaeg ei tohi ületada selle heliraamatu kestust.", + "NoteFolderPicker": "Märkus: juba kaardistatud kaustu ei kuvata", + "NoteRSSFeedPodcastAppsHttps": "Hoiatus: Enamik podcasti rakendusi nõuab, et RSS-voogu URL kasutaks HTTPS-i", + "NoteRSSFeedPodcastAppsPubDate": "Hoiatus: Üks või mitu teie episoodi ei sisalda publikatsioonikuupäeva. Mõned podcasti rakendused nõuavad seda.", + "NoteUploaderFoldersWithMediaFiles": "Kaustu, kus on meediat, käsitletakse eraldi raamatukogu üksustena.", + "NoteUploaderOnlyAudioFiles": "Kui laadite üles ainult helifaile, käsitletakse iga helifaili eraldi heliraamatuna.", + "NoteUploaderUnsupportedFiles": "Toetamata failid jäetakse tähelepanuta. Kausta valimisel või lohistamisel jäetakse tähelepanuta muud failid, mis pole üksuse kaustas.", + "PlaceholderNewCollection": "Uue kogumi nimi", + "PlaceholderNewFolderPath": "Uus kausta tee", + "PlaceholderNewPlaylist": "Uue esitusloendi nimi", + "PlaceholderSearch": "Otsi...", + "PlaceholderSearchEpisode": "Otsi episoodi...", + "ToastAccountUpdateFailed": "Konto värskendamine ebaõnnestus", + "ToastAccountUpdateSuccess": "Konto on värskendatud", + "ToastAuthorImageRemoveFailed": "Pildi eemaldamine ebaõnnestus", + "ToastAuthorImageRemoveSuccess": "Autori pilt on eemaldatud", + "ToastAuthorUpdateFailed": "Autori värskendamine ebaõnnestus", + "ToastAuthorUpdateMerged": "Autor liidetud", + "ToastAuthorUpdateSuccess": "Autor värskendatud", + "ToastAuthorUpdateSuccessNoImageFound": "Autor värskendatud (pilti ei leitud)", + "ToastBackupCreateFailed": "Varukoopia loomine ebaõnnestus", + "ToastBackupCreateSuccess": "Varukoopia loodud", + "ToastBackupDeleteFailed": "Varukoopia kustutamine ebaõnnestus", + "ToastBackupDeleteSuccess": "Varukoopia kustutatud", + "ToastBackupRestoreFailed": "Varukoopia taastamine ebaõnnestus", + "ToastBackupUploadFailed": "Varukoopia üles laadimine ebaõnnestus", + "ToastBackupUploadSuccess": "Varukoopia üles laaditud", + "ToastBatchUpdateFailed": "Partii värskendamine ebaõnnestus", + "ToastBatchUpdateSuccess": "Partii värskendamine õnnestus", + "ToastBookmarkCreateFailed": "Järjehoidja loomine ebaõnnestus", + "ToastBookmarkCreateSuccess": "Järjehoidja lisatud", + "ToastBookmarkRemoveFailed": "Järjehoidja eemaldamine ebaõnnestus", + "ToastBookmarkRemoveSuccess": "Järjehoidja eemaldatud", + "ToastBookmarkUpdateFailed": "Järjehoidja värskendamine ebaõnnestus", + "ToastBookmarkUpdateSuccess": "Järjehoidja värskendatud", + "ToastChaptersHaveErrors": "Peatükkidel on vigu", + "ToastChaptersMustHaveTitles": "Peatükkidel peab olema pealkiri", + "ToastCollectionItemsRemoveFailed": "Üksuse(te) eemaldamine kogumist ebaõnnestus", + "ToastCollectionItemsRemoveSuccess": "Üksus(ed) eemaldatud kogumist", + "ToastCollectionRemoveFailed": "Kogumi eemaldamine ebaõnnestus", + "ToastCollectionRemoveSuccess": "Kogum eemaldatud", + "ToastCollectionUpdateFailed": "Kogumi värskendamine ebaõnnestus", + "ToastCollectionUpdateSuccess": "Kogum värskendatud", + "ToastItemCoverUpdateFailed": "Üksuse kaane värskendamine ebaõnnestus", + "ToastItemCoverUpdateSuccess": "Üksuse kaas värskendatud", + "ToastItemDetailsUpdateFailed": "Üksuse üksikasjade värskendamine ebaõnnestus", + "ToastItemDetailsUpdateSuccess": "Üksuse üksikasjad värskendatud", + "ToastItemDetailsUpdateUnneeded": "Üksuse üksikasjade värskendamine pole vajalik", + "ToastItemMarkedAsFinishedFailed": "Märgistamine kui lõpetatud ebaõnnestus", + "ToastItemMarkedAsFinishedSuccess": "Üksus märgitud kui lõpetatud", + "ToastItemMarkedAsNotFinishedFailed": "Märgistamine kui mitte lõpetatud ebaõnnestus", + "ToastItemMarkedAsNotFinishedSuccess": "Üksus märgitud kui mitte lõpetatud", + "ToastLibraryCreateFailed": "Raamatukogu loomine ebaõnnestus", + "ToastLibraryCreateSuccess": "Raamatukogu \"{0}\" loodud", + "ToastLibraryDeleteFailed": "Raamatukogu kustutamine ebaõnnestus", + "ToastLibraryDeleteSuccess": "Raamatukogu kustutatud", + "ToastLibraryScanFailedToStart": "Skanneerimine ei käivitunud", + "ToastLibraryScanStarted": "Raamatukogu skaneerimine alustatud", + "ToastLibraryUpdateFailed": "Raamatukogu värskendamine ebaõnnestus", + "ToastLibraryUpdateSuccess": "Raamatukogu \"{0}\" värskendatud", + "ToastPlaylistCreateFailed": "Esitusloendi loomine ebaõnnestus", + "ToastPlaylistCreateSuccess": "Esitusloend loodud", + "ToastPlaylistRemoveFailed": "Esitusloendi eemaldamine ebaõnnestus", + "ToastPlaylistRemoveSuccess": "Esitusloend eemaldatud", + "ToastPlaylistUpdateFailed": "Esitusloendi värskendamine ebaõnnestus", + "ToastPlaylistUpdateSuccess": "Esitusloend värskendatud", + "ToastPodcastCreateFailed": "Podcasti loomine ebaõnnestus", + "ToastPodcastCreateSuccess": "Podcast loodud edukalt", + "ToastRemoveItemFromCollectionFailed": "Üksuse eemaldamine kogumist ebaõnnestus", + "ToastRemoveItemFromCollectionSuccess": "Üksus eemaldatud kogumist", + "ToastRSSFeedCloseFailed": "RSS-voogu sulgemine ebaõnnestus", + "ToastRSSFeedCloseSuccess": "RSS-voog suletud", + "ToastSendEbookToDeviceFailed": "E-raamatu saatmine seadmesse ebaõnnestus", + "ToastSendEbookToDeviceSuccess": "E-raamat saadetud seadmesse \"{0}\"", + "ToastSeriesUpdateFailed": "Sarja värskendamine ebaõnnestus", + "ToastSeriesUpdateSuccess": "Sarja värskendamine õnnestus", + "ToastSessionDeleteFailed": "Seansi kustutamine ebaõnnestus", + "ToastSessionDeleteSuccess": "Sessioon kustutatud", + "ToastSocketConnected": "Pesa ühendatud", + "ToastSocketDisconnected": "Pesa ühendus katkenud", + "ToastSocketFailedToConnect": "Pesa ühendamine ebaõnnestus", + "ToastUserDeleteFailed": "Kasutaja kustutamine ebaõnnestus", + "ToastUserDeleteSuccess": "Kasutaja kustutatud" +} \ No newline at end of file From e368ffe29f837ee00787b41012e9a01e9862b970 Mon Sep 17 00:00:00 2001 From: Teekeks <info@teawork.de> Date: Thu, 22 Feb 2024 19:20:49 +0100 Subject: [PATCH 0395/2145] feat(i18n): added missing translatable string in player ui --- client/components/player/PlayerUi.vue | 2 +- client/strings/en-us.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue index e1b0f96d..bca3e7ee 100644 --- a/client/components/player/PlayerUi.vue +++ b/client/components/player/PlayerUi.vue @@ -53,7 +53,7 @@ <p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p> <div class="flex-grow" /> <p class="text-xs sm:text-sm text-gray-300 pt-0.5"> - {{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400"> ({{ currentChapterIndex + 1 }} of {{ chapters.length }})</span> + {{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400"> {{ $setString('LabelPlayerChaperMarker', [currentChapterIndex + 1, chapters.length]) }}</span> </p> <div class="flex-grow" /> <p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p> diff --git a/client/strings/en-us.json b/client/strings/en-us.json index e3349d1f..0df642a7 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -386,6 +386,7 @@ "LabelPermissionsUpdate": "Can Update", "LabelPermissionsUpload": "Can Upload", "LabelPhotoPathURL": "Photo Path/URL", + "LabelPlayerChaperMarker": "({0} of {1})", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Play Method", "LabelPodcast": "Podcast", From def2b6425ba82a7dd50613431fdaa14e7555130e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 22 Feb 2024 16:30:41 -0600 Subject: [PATCH 0396/2145] Update:Username and password inputs on login page trim whitespace #2628 --- client/pages/login.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index c7efa0d0..9b7758d8 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -10,9 +10,9 @@ <form @submit.prevent="submitServerSetup"> <p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p> - <ui-text-input-with-label v-model="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" /> - <ui-text-input-with-label v-model="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" /> - <ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" /> + <ui-text-input-with-label v-model.trim="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" /> + <ui-text-input-with-label v-model.trim="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" /> + <ui-text-input-with-label v-model.trim="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" /> <p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p> <ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" /> @@ -34,10 +34,10 @@ <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" /> + <ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" /> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label> - <ui-text-input v-model="password" type="password" :disabled="processing" class="w-full mb-3" /> + <ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" /> <div class="w-full flex justify-end py-3"> <ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn> </div> From 608b25de45e9a02836326686aa85b4fea08228b6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 23 Feb 2024 16:59:46 -0600 Subject: [PATCH 0397/2145] Map en-us translations --- client/strings/cs.json | 9 +++++++++ client/strings/da.json | 9 +++++++++ client/strings/de.json | 4 +++- client/strings/en-us.json | 2 ++ client/strings/es.json | 9 +++++++++ client/strings/et.json | 9 +++++++++ client/strings/fr.json | 9 +++++++++ client/strings/gu.json | 9 +++++++++ client/strings/hi.json | 9 +++++++++ client/strings/hr.json | 9 +++++++++ client/strings/hu.json | 9 +++++++++ client/strings/it.json | 11 ++++++++++- client/strings/lt.json | 9 +++++++++ client/strings/nl.json | 9 +++++++++ client/strings/no.json | 9 +++++++++ client/strings/pl.json | 9 +++++++++ client/strings/pt-br.json | 11 ++++++++++- client/strings/ru.json | 9 +++++++++ client/strings/sv.json | 9 +++++++++ client/strings/zh-cn.json | 9 +++++++++ 20 files changed, 169 insertions(+), 3 deletions(-) diff --git a/client/strings/cs.json b/client/strings/cs.json index 3ab713ef..3134d60d 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Spárovat všechny autory", "ButtonMatchBooks": "Spárovat Knihy", "ButtonNevermind": "Nevadí", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Otevřít kanál", @@ -51,6 +52,7 @@ "ButtonPlay": "Přehrát", "ButtonPlaying": "Hraje", "ButtonPlaylists": "Seznamy skladeb", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť", "ButtonPurgeItemsCache": "Vyčistit mezipaměť položek", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Odstranit z fronty", "ButtonQuickMatch": "Rychlé přiřazení", "ButtonRead": "Číst", + "ButtonRefresh": "Refresh", "ButtonRemove": "Odstranit", "ButtonRemoveAll": "Odstranit vše", "ButtonRemoveAllLibraryItems": "Odstranit všechny položky knihovny", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Vybrat cestu ke složce", "ButtonSeries": "Série", "ButtonSetChaptersFromTracks": "Nastavit kapitoly ze stop", + "ButtonShare": "Share", "ButtonShiftTimes": "Časy posunu", "ButtonShow": "Zobrazit", "ButtonStartM4BEncode": "Spustit kódování M4B", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Aktualizovat podrobnosti", "HeaderUpdateLibrary": "Aktualizovat knihovnu", "HeaderUsers": "Uživatelé", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Vaše statistiky", "LabelAbridged": "Zkráceno", "LabelAccountType": "Typ účtu", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Může stahovat", "LabelPermissionsUpdate": "Může aktualizovat", "LabelPermissionsUpload": "Může nahrávat", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Cesta k fotografii/URL", "LabelPlaylists": "Seznamy skladeb", "LabelPlayMethod": "Metoda přehrávání", @@ -436,6 +442,7 @@ "LabelSeries": "Série", "LabelSeriesName": "Název série", "LabelSeriesProgress": "Průběh série", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Nastavit jako primární", "LabelSetEbookAsSupplementary": "Nastavit jako doplňkové", "LabelSettingsAudiobooksOnly": "Pouze audioknihy", @@ -552,6 +559,8 @@ "LabelViewQueue": "Zobrazit frontu přehrávače", "LabelVolume": "Hlasitost", "LabelWeekdaysToRun": "Dny v týdnu ke spuštění", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Doba trvání vaší audioknihy", "LabelYourBookmarks": "Vaše záložky", "LabelYourPlaylists": "Vaše seznamy přehrávání", diff --git a/client/strings/da.json b/client/strings/da.json index d456cd51..f79f19b1 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Match alle forfattere", "ButtonMatchBooks": "Match bøger", "ButtonNevermind": "Glem det", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "OK", "ButtonOpenFeed": "Åbn feed", @@ -51,6 +52,7 @@ "ButtonPlay": "Afspil", "ButtonPlaying": "Afspiller", "ButtonPlaylists": "Afspilningslister", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Ryd al cache", "ButtonPurgeItemsCache": "Ryd elementcache", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Fjern fra kø", "ButtonQuickMatch": "Hurtig Match", "ButtonRead": "Læs", + "ButtonRefresh": "Refresh", "ButtonRemove": "Fjern", "ButtonRemoveAll": "Fjern Alle", "ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Vælg Mappen Sti", "ButtonSeries": "Serie", "ButtonSetChaptersFromTracks": "Sæt kapitler fra spor", + "ButtonShare": "Share", "ButtonShiftTimes": "Skift Tider", "ButtonShow": "Vis", "ButtonStartM4BEncode": "Start M4B Kode", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Opdater Detaljer", "HeaderUpdateLibrary": "Opdater Bibliotek", "HeaderUsers": "Brugere", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Dine Statistikker", "LabelAbridged": "Abridged", "LabelAccountType": "Kontotype", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Kan downloade", "LabelPermissionsUpdate": "Kan opdatere", "LabelPermissionsUpload": "Kan uploade", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Foto sti/URL", "LabelPlaylists": "Afspilningslister", "LabelPlayMethod": "Afspilningsmetode", @@ -436,6 +442,7 @@ "LabelSeries": "Serie", "LabelSeriesName": "Serienavn", "LabelSeriesProgress": "Seriefremskridt", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Indstil som primær", "LabelSetEbookAsSupplementary": "Indstil som supplerende", "LabelSettingsAudiobooksOnly": "Kun lydbøger", @@ -552,6 +559,8 @@ "LabelViewQueue": "Se afspilningskø", "LabelVolume": "Volumen", "LabelWeekdaysToRun": "Ugedage til kørsel", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Din lydbogsvarighed", "LabelYourBookmarks": "Dine bogmærker", "LabelYourPlaylists": "Dine spillelister", diff --git a/client/strings/de.json b/client/strings/de.json index 546631f3..46467734 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)", "ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)", "ButtonNevermind": "Abbrechen", + "ButtonNext": "Next", "ButtonNextChapter": "Nächstes Kapitel", "ButtonOk": "Ok", "ButtonOpenFeed": "Feed öffnen", @@ -51,6 +52,7 @@ "ButtonPlay": "Abspielen", "ButtonPlaying": "Spielt", "ButtonPlaylists": "Wiedergabelisten", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Vorheriges Kapitel", "ButtonPurgeAllCache": "Cache leeren", "ButtonPurgeItemsCache": "Lösche Medien-Cache", @@ -772,4 +774,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 faa8cdcc..b8860ffa 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Match All Authors", "ButtonMatchBooks": "Match Books", "ButtonNevermind": "Nevermind", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Open Feed", @@ -51,6 +52,7 @@ "ButtonPlay": "Play", "ButtonPlaying": "Playing", "ButtonPlaylists": "Playlists", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Purge All Cache", "ButtonPurgeItemsCache": "Purge Items Cache", diff --git a/client/strings/es.json b/client/strings/es.json index 4b85106c..75052413 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Encontrar Todos los Autores", "ButtonMatchBooks": "Encontrar Libros", "ButtonNevermind": "Olvidar", + "ButtonNext": "Next", "ButtonNextChapter": "Siguiente Capítulo", "ButtonOk": "Ok", "ButtonOpenFeed": "Abrir Fuente", @@ -51,6 +52,7 @@ "ButtonPlay": "Reproducir", "ButtonPlaying": "Reproduciendo", "ButtonPlaylists": "Listas de Reproducción", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Capítulo Anterior", "ButtonPurgeAllCache": "Purgar Todo el Cache", "ButtonPurgeItemsCache": "Purgar Elementos de Cache", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Remover de la Fila", "ButtonQuickMatch": "Encontrar Rápido", "ButtonRead": "Leer", + "ButtonRefresh": "Refresh", "ButtonRemove": "Remover", "ButtonRemoveAll": "Remover Todos", "ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Seleccionar Ruta de Carpeta", "ButtonSeries": "Series", "ButtonSetChaptersFromTracks": "Seleccionar Capítulos Según las Pistas", + "ButtonShare": "Share", "ButtonShiftTimes": "Desplazar Tiempos", "ButtonShow": "Mostrar", "ButtonStartM4BEncode": "Iniciar Codificación M4B", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Actualizar Detalles", "HeaderUpdateLibrary": "Actualizar Biblioteca", "HeaderUsers": "Usuarios", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Tus Estadísticas", "LabelAbridged": "Abreviado", "LabelAccountType": "Tipo de Cuenta", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Puede Descargar", "LabelPermissionsUpdate": "Puede Actualizar", "LabelPermissionsUpload": "Puede Subir", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Ruta de Acceso/URL de Foto", "LabelPlaylists": "Lista de Reproducción", "LabelPlayMethod": "Método de Reproducción", @@ -436,6 +442,7 @@ "LabelSeries": "Series", "LabelSeriesName": "Nombre de la Serie", "LabelSeriesProgress": "Progreso de la Serie", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Establecer como primario", "LabelSetEbookAsSupplementary": "Establecer como suplementario", "LabelSettingsAudiobooksOnly": "Sólo Audiolibros", @@ -552,6 +559,8 @@ "LabelViewQueue": "Ver Fila del Reproductor", "LabelVolume": "Volumen", "LabelWeekdaysToRun": "Correr en Días de la Semana", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Duración de tu Audiolibro", "LabelYourBookmarks": "Tus Marcadores", "LabelYourPlaylists": "Tus Listas", diff --git a/client/strings/et.json b/client/strings/et.json index f0641705..07f9eef0 100644 --- a/client/strings/et.json +++ b/client/strings/et.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Sobita kõik autorid", "ButtonMatchBooks": "Sobita raamatud", "ButtonNevermind": "Pole tähtis", + "ButtonNext": "Next", "ButtonNextChapter": "Järgmine peatükk", "ButtonOk": "Ok", "ButtonOpenFeed": "Ava voog", @@ -51,6 +52,7 @@ "ButtonPlay": "Mängi", "ButtonPlaying": "Mängib", "ButtonPlaylists": "Esitusloendid", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Eelmine peatükk", "ButtonPurgeAllCache": "Tühjenda kogu vahemälu", "ButtonPurgeItemsCache": "Tühjenda esemete vahemälu", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Eemalda järjekorrast", "ButtonQuickMatch": "Kiire sobitamine", "ButtonRead": "Loe", + "ButtonRefresh": "Refresh", "ButtonRemove": "Eemalda", "ButtonRemoveAll": "Eemalda kõik", "ButtonRemoveAllLibraryItems": "Eemalda kõik raamatukogu esemed", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Vali kaustatee", "ButtonSeries": "Sarjad", "ButtonSetChaptersFromTracks": "Määra peatükid lugudest", + "ButtonShare": "Share", "ButtonShiftTimes": "Nihke ajad", "ButtonShow": "Näita", "ButtonStartM4BEncode": "Alusta M4B kodeerimist", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Uuenda detaile", "HeaderUpdateLibrary": "Uuenda raamatukogu", "HeaderUsers": "Kasutajad", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Sinu statistika", "LabelAbridged": "Kärbitud", "LabelAccountType": "Konto tüüp", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Saab alla laadida", "LabelPermissionsUpdate": "Saab uuendada", "LabelPermissionsUpload": "Saab üles laadida", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Foto tee/URL", "LabelPlaylists": "Mänguloendid", "LabelPlayMethod": "Esitusmeetod", @@ -436,6 +442,7 @@ "LabelSeries": "Seeria", "LabelSeriesName": "Seeria nimi", "LabelSeriesProgress": "Seeria edenemine", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Määra peamiseks", "LabelSetEbookAsSupplementary": "Määra täiendavaks", "LabelSettingsAudiobooksOnly": "Ainult heliraamatud", @@ -552,6 +559,8 @@ "LabelViewQueue": "Vaata esitusjärjekorda", "LabelVolume": "Heli tugevus", "LabelWeekdaysToRun": "Päevad nädalas käivitamiseks", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Teie heliraamatu kestus", "LabelYourBookmarks": "Teie järjehoidjad", "LabelYourPlaylists": "Teie esitusloendid", diff --git a/client/strings/fr.json b/client/strings/fr.json index 3f04fab8..bbf38e18 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Chercher tous les auteurs", "ButtonMatchBooks": "Chercher les livres", "ButtonNevermind": "Non merci", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Ouvrir le flux", @@ -51,6 +52,7 @@ "ButtonPlay": "Écouter", "ButtonPlaying": "En lecture", "ButtonPlaylists": "Listes de lecture", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Purger le cache", "ButtonPurgeItemsCache": "Purger le cache des articles", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Supprimer de la liste de lecture", "ButtonQuickMatch": "Recherche rapide", "ButtonRead": "Lire", + "ButtonRefresh": "Refresh", "ButtonRemove": "Supprimer", "ButtonRemoveAll": "Supprimer tout", "ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Sélectionner le chemin du dossier", "ButtonSeries": "Séries", "ButtonSetChaptersFromTracks": "Positionner les chapitres par rapports aux pistes", + "ButtonShare": "Share", "ButtonShiftTimes": "Décaler l’horodatage du livre", "ButtonShow": "Afficher", "ButtonStartM4BEncode": "Démarrer l’encodage M4B", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Mettre à jour les détails", "HeaderUpdateLibrary": "Mettre à jour la bibliothèque", "HeaderUsers": "Utilisateurs", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Vos statistiques", "LabelAbridged": "Version courte", "LabelAccountType": "Type de compte", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Peut télécharger", "LabelPermissionsUpdate": "Peut mettre à jour", "LabelPermissionsUpload": "Peut téléverser", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Chemin / URL des photos", "LabelPlaylists": "Listes de lecture", "LabelPlayMethod": "Méthode d’écoute", @@ -436,6 +442,7 @@ "LabelSeries": "Séries", "LabelSeriesName": "Nom de la série", "LabelSeriesProgress": "Progression de séries", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Définir comme principale", "LabelSetEbookAsSupplementary": "Définir comme supplémentaire", "LabelSettingsAudiobooksOnly": "Livres audios seulement", @@ -552,6 +559,8 @@ "LabelViewQueue": "Afficher la liste de lecture", "LabelVolume": "Volume", "LabelWeekdaysToRun": "Jours de la semaine à exécuter", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Durée de vos livres audios", "LabelYourBookmarks": "Vos signets", "LabelYourPlaylists": "Vos listes de lecture", diff --git a/client/strings/gu.json b/client/strings/gu.json index daa923db..2f3d9cd8 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "બધા મેળ ખાતા લેખકો શોધો", "ButtonMatchBooks": "મેળ ખાતી પુસ્તકો શોધો", "ButtonNevermind": "કંઈ વાંધો નહીં", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "ઓકે", "ButtonOpenFeed": "ફીડ ખોલો", @@ -51,6 +52,7 @@ "ButtonPlay": "ચલાવો", "ButtonPlaying": "ચલાવી રહ્યું છે", "ButtonPlaylists": "પ્લેલિસ્ટ", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "બધો Cache કાઢી નાખો", "ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "કતારથી કાઢી નાખો", "ButtonQuickMatch": "ઝડપી મેળ ખવડાવો", "ButtonRead": "વાંચો", + "ButtonRefresh": "Refresh", "ButtonRemove": "કાઢી નાખો", "ButtonRemoveAll": "બધું કાઢી નાખો", "ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "ફોલ્ડર પથ પસંદ કરો", "ButtonSeries": "સિરીઝ", "ButtonSetChaptersFromTracks": "ટ્રેક્સથી પ્રકરણો સેટ કરો", + "ButtonShare": "Share", "ButtonShiftTimes": "સમય શિફ્ટ કરો", "ButtonShow": "બતાવો", "ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Update Details", "HeaderUpdateLibrary": "Update Library", "HeaderUsers": "Users", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Your Stats", "LabelAbridged": "Abridged", "LabelAccountType": "Account Type", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Can Download", "LabelPermissionsUpdate": "Can Update", "LabelPermissionsUpload": "Can Upload", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Photo Path/URL", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Play Method", @@ -436,6 +442,7 @@ "LabelSeries": "Series", "LabelSeriesName": "Series Name", "LabelSeriesProgress": "Series Progress", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSettingsAudiobooksOnly": "Audiobooks only", @@ -552,6 +559,8 @@ "LabelViewQueue": "View player queue", "LabelVolume": "Volume", "LabelWeekdaysToRun": "Weekdays to run", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Your audiobook duration", "LabelYourBookmarks": "Your Bookmarks", "LabelYourPlaylists": "Your Playlists", diff --git a/client/strings/hi.json b/client/strings/hi.json index a59b43ec..01667b59 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "सभी लेखकों को तलाश करें", "ButtonMatchBooks": "संबंधित पुस्तकों का मिलान करें", "ButtonNevermind": "कोई बात नहीं", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "ठीक है", "ButtonOpenFeed": "फ़ीड खोलें", @@ -51,6 +52,7 @@ "ButtonPlay": "चलाएँ", "ButtonPlaying": "चल रही है", "ButtonPlaylists": "प्लेलिस्ट्स", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "सभी Cache मिटाएं", "ButtonPurgeItemsCache": "आइटम Cache मिटाएं", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "कतार से हटाएं", "ButtonQuickMatch": "जल्दी से समानता की तलाश करें", "ButtonRead": "पढ़ लिया", + "ButtonRefresh": "Refresh", "ButtonRemove": "हटाएं", "ButtonRemoveAll": "सभी हटाएं", "ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "फ़ोल्डर का पथ चुनें", "ButtonSeries": "सीरीज", "ButtonSetChaptersFromTracks": "ट्रैक्स से अध्याय बनाएं", + "ButtonShare": "Share", "ButtonShiftTimes": "समय खिसकाए", "ButtonShow": "दिखाएं", "ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Update Details", "HeaderUpdateLibrary": "Update Library", "HeaderUsers": "Users", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Your Stats", "LabelAbridged": "Abridged", "LabelAccountType": "Account Type", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Can Download", "LabelPermissionsUpdate": "Can Update", "LabelPermissionsUpload": "Can Upload", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Photo Path/URL", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Play Method", @@ -436,6 +442,7 @@ "LabelSeries": "Series", "LabelSeriesName": "Series Name", "LabelSeriesProgress": "Series Progress", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSettingsAudiobooksOnly": "Audiobooks only", @@ -552,6 +559,8 @@ "LabelViewQueue": "View player queue", "LabelVolume": "Volume", "LabelWeekdaysToRun": "Weekdays to run", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Your audiobook duration", "LabelYourBookmarks": "Your Bookmarks", "LabelYourPlaylists": "Your Playlists", diff --git a/client/strings/hr.json b/client/strings/hr.json index 6e105ca2..8aa933c7 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Matchaj sve autore", "ButtonMatchBooks": "Matchaj knjige", "ButtonNevermind": "Nije bitno", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Otvori feed", @@ -51,6 +52,7 @@ "ButtonPlay": "Pokreni", "ButtonPlaying": "Playing", "ButtonPlaylists": "Playlists", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Isprazni sav cache", "ButtonPurgeItemsCache": "Isprazni Items Cache", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Remove from queue", "ButtonQuickMatch": "Brzi match", "ButtonRead": "Pročitaj", + "ButtonRefresh": "Refresh", "ButtonRemove": "Ukloni", "ButtonRemoveAll": "Ukloni sve", "ButtonRemoveAllLibraryItems": "Ukloni sve stvari iz biblioteke", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Odaberi putanju do folder", "ButtonSeries": "Serije", "ButtonSetChaptersFromTracks": "Set chapters from tracks", + "ButtonShare": "Share", "ButtonShiftTimes": "Pomakni vremena", "ButtonShow": "Prikaži", "ButtonStartM4BEncode": "Pokreni M4B kodiranje", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Aktualiziraj detalje", "HeaderUpdateLibrary": "Aktualiziraj biblioteku", "HeaderUsers": "Korinici", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Tvoja statistika", "LabelAbridged": "Abridged", "LabelAccountType": "Vrsta korisničkog računa", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Smije preuzimati", "LabelPermissionsUpdate": "Smije aktualizirati", "LabelPermissionsUpload": "Smije uploadati", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Slika putanja/URL", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Vrsta reprodukcije", @@ -436,6 +442,7 @@ "LabelSeries": "Serije", "LabelSeriesName": "Ime serije", "LabelSeriesProgress": "Series Progress", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSettingsAudiobooksOnly": "Audiobooks only", @@ -552,6 +559,8 @@ "LabelViewQueue": "View player queue", "LabelVolume": "Volume", "LabelWeekdaysToRun": "Radnih dana da radi", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Tvoje trajanje audiobooka", "LabelYourBookmarks": "Tvoje knjižne oznake", "LabelYourPlaylists": "Your Playlists", diff --git a/client/strings/hu.json b/client/strings/hu.json index 9d79f28e..d52957e1 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Minden szerző egyeztetése", "ButtonMatchBooks": "Könyvek egyeztetése", "ButtonNevermind": "Mindegy", + "ButtonNext": "Next", "ButtonNextChapter": "Következő fejezet", "ButtonOk": "Oké", "ButtonOpenFeed": "Hírcsatorna megnyitása", @@ -51,6 +52,7 @@ "ButtonPlay": "Lejátszás", "ButtonPlaying": "Lejátszás folyamatban", "ButtonPlaylists": "Lejátszási listák", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Előző fejezet", "ButtonPurgeAllCache": "Összes gyorsítótár törlése", "ButtonPurgeItemsCache": "Elemek gyorsítótárának törlése", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Eltávolítás a sorból", "ButtonQuickMatch": "Gyors egyeztetés", "ButtonRead": "Olvasás", + "ButtonRefresh": "Refresh", "ButtonRemove": "Eltávolítás", "ButtonRemoveAll": "Összes eltávolítása", "ButtonRemoveAllLibraryItems": "Összes könyvtárelem eltávolítása", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Mappa útvonalának kiválasztása", "ButtonSeries": "Sorozatok", "ButtonSetChaptersFromTracks": "Fejezetek beállítása sávokból", + "ButtonShare": "Share", "ButtonShiftTimes": "Idők eltolása", "ButtonShow": "Megjelenítés", "ButtonStartM4BEncode": "M4B kódolás indítása", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Részletek frissítése", "HeaderUpdateLibrary": "Könyvtár frissítése", "HeaderUsers": "Felhasználók", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Saját statisztikák", "LabelAbridged": "Tömörített", "LabelAccountType": "Fióktípus", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Letölthet", "LabelPermissionsUpdate": "Frissíthet", "LabelPermissionsUpload": "Feltölthet", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Fénykép útvonal/URL", "LabelPlaylists": "Lejátszási listák", "LabelPlayMethod": "Lejátszási módszer", @@ -436,6 +442,7 @@ "LabelSeries": "Sorozat", "LabelSeriesName": "Sorozat neve", "LabelSeriesProgress": "Sorozat haladása", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Beállítás elsődlegesként", "LabelSetEbookAsSupplementary": "Beállítás kiegészítőként", "LabelSettingsAudiobooksOnly": "Csak hangoskönyvek", @@ -552,6 +559,8 @@ "LabelViewQueue": "Lejátszó sor megtekintése", "LabelVolume": "Hangerő", "LabelWeekdaysToRun": "Futás napjai", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Hangoskönyv időtartama", "LabelYourBookmarks": "Könyvjelzőid", "LabelYourPlaylists": "Lejátszási listáid", diff --git a/client/strings/it.json b/client/strings/it.json index 11865aad..26a61fda 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Aggiungi metadata agli Autori", "ButtonMatchBooks": "Aggiungi metadata della Libreria", "ButtonNevermind": "Nevermind", + "ButtonNext": "Next", "ButtonNextChapter": "Prossimo Capitolo", "ButtonOk": "Ok", "ButtonOpenFeed": "Apri Feed", @@ -51,6 +52,7 @@ "ButtonPlay": "Play", "ButtonPlaying": "In Riproduzione", "ButtonPlaylists": "Playlists", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Capitolo Precendente", "ButtonPurgeAllCache": "Elimina tutta la Cache", "ButtonPurgeItemsCache": "Elimina la Cache selezionata", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Rimuovi dalla Coda", "ButtonQuickMatch": "Controlla Metadata Auto", "ButtonRead": "Leggi", + "ButtonRefresh": "Refresh", "ButtonRemove": "Rimuovi", "ButtonRemoveAll": "Rimuovi Tutto", "ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Seleziona percorso cartella", "ButtonSeries": "Serie", "ButtonSetChaptersFromTracks": "Impostare i capitoli dalle tracce", + "ButtonShare": "Share", "ButtonShiftTimes": "Ricerca veloce", "ButtonShow": "Mostra", "ButtonStartM4BEncode": "Inizia L'Encode del M4B", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Aggiorna Dettagli", "HeaderUpdateLibrary": "Aggiorna Libreria", "HeaderUsers": "Utenti", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Statistiche Personali", "LabelAbridged": "Abbreviato", "LabelAccountType": "Tipo di Account", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Può Scaricare", "LabelPermissionsUpdate": "Può Aggiornare", "LabelPermissionsUpload": "Può caricare", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "foto Path/URL", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Metodo di riproduzione", @@ -436,6 +442,7 @@ "LabelSeries": "Serie", "LabelSeriesName": "Nome Serie", "LabelSeriesProgress": "Cominciato", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Immposta come Primario", "LabelSetEbookAsSupplementary": "Imposta come Suplementare", "LabelSettingsAudiobooksOnly": "Solo Audiolibri", @@ -552,6 +559,8 @@ "LabelViewQueue": "Visualizza coda", "LabelVolume": "Volume", "LabelWeekdaysToRun": "Giorni feriali da eseguire", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "La durata dell'audiolibro", "LabelYourBookmarks": "I tuoi Preferiti", "LabelYourPlaylists": "le tue Playlist", @@ -765,4 +774,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 f98d5aee..1c2558c9 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Pritaikyti visus autorius", "ButtonMatchBooks": "Pritaikyti knygas", "ButtonNevermind": "Nesvarbu", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Atidaryti srautą", @@ -51,6 +52,7 @@ "ButtonPlay": "Groti", "ButtonPlaying": "Grojama", "ButtonPlaylists": "Grojaraščiai", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Valyti visą saugyklą", "ButtonPurgeItemsCache": "Valyti elementų saugyklą", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Pašalinti iš eilės", "ButtonQuickMatch": "Greitas pritaikymas", "ButtonRead": "Skaityti", + "ButtonRefresh": "Refresh", "ButtonRemove": "Pašalinti", "ButtonRemoveAll": "Pašalinti viską", "ButtonRemoveAllLibraryItems": "Pašalinti visus bibliotekos elementus", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Pasirinkti aplanko kelią", "ButtonSeries": "Serijos", "ButtonSetChaptersFromTracks": "Nustatyti skyrius iš takelių", + "ButtonShare": "Share", "ButtonShiftTimes": "Perstumti laikus", "ButtonShow": "Rodyti", "ButtonStartM4BEncode": "Pradėti M4B kodavimą", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Atnaujinti informaciją", "HeaderUpdateLibrary": "Atnaujinti biblioteką", "HeaderUsers": "Naudotojai", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Jūsų statistika", "LabelAbridged": "Santrauka", "LabelAccountType": "Paskyros tipas", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Gali atsisiųsti", "LabelPermissionsUpdate": "Gali atnaujinti", "LabelPermissionsUpload": "Gali įkelti", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Nuotraukos kelias/URL", "LabelPlaylists": "Grojaraščiai", "LabelPlayMethod": "Grojimo metodas", @@ -436,6 +442,7 @@ "LabelSeries": "Serija", "LabelSeriesName": "Serijos pavadinimas", "LabelSeriesProgress": "Serijos progresas", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Nustatyti kaip pagrindinę", "LabelSetEbookAsSupplementary": "Nustatyti kaip papildomą", "LabelSettingsAudiobooksOnly": "Tik garso knygos", @@ -552,6 +559,8 @@ "LabelViewQueue": "Peržiūrėti grotuvo eilę", "LabelVolume": "Garsumas", "LabelWeekdaysToRun": "Dienos, kuriomis vykdyti", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Jūsų garso knygos trukmė", "LabelYourBookmarks": "Jūsų skirtukai", "LabelYourPlaylists": "Jūsų grojaraščiai", diff --git a/client/strings/nl.json b/client/strings/nl.json index 1c1ffa1b..dd06146d 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Alle auteurs matchen", "ButtonMatchBooks": "Alle boeken matchen", "ButtonNevermind": "Laat maar", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Feed openen", @@ -51,6 +52,7 @@ "ButtonPlay": "Afspelen", "ButtonPlaying": "Speelt", "ButtonPlaylists": "Afspeellijsten", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Volledige cache legen", "ButtonPurgeItemsCache": "Onderdelen-cache legen", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Uit wachtrij verwijderen", "ButtonQuickMatch": "Snelle match", "ButtonRead": "Lees", + "ButtonRefresh": "Refresh", "ButtonRemove": "Verwijder", "ButtonRemoveAll": "Alles verwijderen", "ButtonRemoveAllLibraryItems": "Verwijder volledige bibliotheekinhoud", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Maplocatie selecteren", "ButtonSeries": "Series", "ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks", + "ButtonShare": "Share", "ButtonShiftTimes": "Tijden verschuiven", "ButtonShow": "Toon", "ButtonStartM4BEncode": "Start M4B-encoding", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Details bijwerken", "HeaderUpdateLibrary": "Bibliotheek bijwerken", "HeaderUsers": "Gebruikers", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Je statistieken", "LabelAbridged": "Verkort", "LabelAccountType": "Accounttype", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Kan downloaden", "LabelPermissionsUpdate": "Kan bijwerken", "LabelPermissionsUpload": "Kan uploaden", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Foto pad/URL", "LabelPlaylists": "Afspeellijsten", "LabelPlayMethod": "Afspeelwijze", @@ -436,6 +442,7 @@ "LabelSeries": "Serie", "LabelSeriesName": "Naam serie", "LabelSeriesProgress": "Voortgang serie", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Stel in als primair", "LabelSetEbookAsSupplementary": "Stel in als supplementair", "LabelSettingsAudiobooksOnly": "Alleen audiobooks", @@ -552,6 +559,8 @@ "LabelViewQueue": "Bekijk afspeelwachtrij", "LabelVolume": "Volume", "LabelWeekdaysToRun": "Weekdagen om te draaien", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Je audioboekduur", "LabelYourBookmarks": "Je boekwijzers", "LabelYourPlaylists": "Je afspeellijsten", diff --git a/client/strings/no.json b/client/strings/no.json index 432115df..a29a7ef2 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Søk opp alle forfattere", "ButtonMatchBooks": "Søk opp bøker", "ButtonNevermind": "Avbryt", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Åpne Feed", @@ -51,6 +52,7 @@ "ButtonPlay": "Spill av", "ButtonPlaying": "Spiller av", "ButtonPlaylists": "Spilleliste", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Tøm alle mellomlager", "ButtonPurgeItemsCache": "Tøm mellomlager", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Fjern fra kø", "ButtonQuickMatch": "Kjapt søk", "ButtonRead": "Les", + "ButtonRefresh": "Refresh", "ButtonRemove": "Fjern", "ButtonRemoveAll": "Fjern alle", "ButtonRemoveAllLibraryItems": "Fjern alle bibliotekobjekter", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Velg mappe", "ButtonSeries": "Serier", "ButtonSetChaptersFromTracks": "Sett kapittel fra spor", + "ButtonShare": "Share", "ButtonShiftTimes": "Forskyv tider", "ButtonShow": "Vis", "ButtonStartM4BEncode": "Start M4B Koding", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Oppdater detaljer", "HeaderUpdateLibrary": "Oppdater bibliotek", "HeaderUsers": "Brukere", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Din statistikk", "LabelAbridged": "Forkortet", "LabelAccountType": "Kontotype", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Kan laste ned", "LabelPermissionsUpdate": "Kan oppdatere", "LabelPermissionsUpload": "Kan laste opp", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Bilde sti/URL", "LabelPlaylists": "Spilleliste", "LabelPlayMethod": "Avspillingsmetode", @@ -436,6 +442,7 @@ "LabelSeries": "Serier", "LabelSeriesName": "Serier Navn", "LabelSeriesProgress": "Serier fremgang", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Sett som primær", "LabelSetEbookAsSupplementary": "Sett som supplerende", "LabelSettingsAudiobooksOnly": "Kun lydbøker", @@ -552,6 +559,8 @@ "LabelViewQueue": "Vis spillerkø", "LabelVolume": "Volum", "LabelWeekdaysToRun": "Ukedager å kjøre", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Din lydbok lengde", "LabelYourBookmarks": "Dine bokmerker", "LabelYourPlaylists": "Dine spillelister", diff --git a/client/strings/pl.json b/client/strings/pl.json index 7f0e4497..3189ccb7 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Dopasuj wszystkich autorów", "ButtonMatchBooks": "Dopasuj książki", "ButtonNevermind": "Anuluj", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Otwórz feed", @@ -51,6 +52,7 @@ "ButtonPlay": "Odtwarzaj", "ButtonPlaying": "Odtwarzane", "ButtonPlaylists": "Playlists", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Wyczyść dane tymczasowe", "ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Usuń z kolejki", "ButtonQuickMatch": "Szybkie dopasowanie", "ButtonRead": "Czytaj", + "ButtonRefresh": "Refresh", "ButtonRemove": "Usuń", "ButtonRemoveAll": "Usuń wszystko", "ButtonRemoveAllLibraryItems": "Usuń wszystkie elementy z biblioteki", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Wybierz ścieżkę folderu", "ButtonSeries": "Seria", "ButtonSetChaptersFromTracks": "Set chapters from tracks", + "ButtonShare": "Share", "ButtonShiftTimes": "Przesunięcie czasowe", "ButtonShow": "Pokaż", "ButtonStartM4BEncode": "Eksportuj jako plik M4B", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Zaktualizuj szczegóły", "HeaderUpdateLibrary": "Zaktualizuj bibliotekę", "HeaderUsers": "Użytkownicy", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Twoje statystyki", "LabelAbridged": "Abridged", "LabelAccountType": "Typ konta", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Ma możliwość pobierania", "LabelPermissionsUpdate": "Ma możliwość aktualizowania", "LabelPermissionsUpload": "Ma możliwość dodawania", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Scieżka/URL do zdjęcia", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Metoda odtwarzania", @@ -436,6 +442,7 @@ "LabelSeries": "Serie", "LabelSeriesName": "Nazwy serii", "LabelSeriesProgress": "Postęp w serii", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSettingsAudiobooksOnly": "Audiobooks only", @@ -552,6 +559,8 @@ "LabelViewQueue": "Wyświetlaj kolejkę odtwarzania", "LabelVolume": "Głośność", "LabelWeekdaysToRun": "Dni tygodnia", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Czas trwania audiobooka", "LabelYourBookmarks": "Twoje zakładki", "LabelYourPlaylists": "Your Playlists", diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index fc626aff..14e7f52a 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Consultar Todos os Autores", "ButtonMatchBooks": "Consultar Livros", "ButtonNevermind": "Cancelar", + "ButtonNext": "Next", "ButtonNextChapter": "Próximo Capítulo", "ButtonOk": "Ok", "ButtonOpenFeed": "Abrir Feed", @@ -51,6 +52,7 @@ "ButtonPlay": "Reproduzir", "ButtonPlaying": "Reproduzindo", "ButtonPlaylists": "Lista de Reprodução", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Capítulo Anterior", "ButtonPurgeAllCache": "Apagar Todo o Cache", "ButtonPurgeItemsCache": "Apagar o Cache de Itens", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Remover da Lista", "ButtonQuickMatch": "Consulta rápida", "ButtonRead": "Ler", + "ButtonRefresh": "Refresh", "ButtonRemove": "Remover", "ButtonRemoveAll": "Remover Todos", "ButtonRemoveAllLibraryItems": "Remover Todos os Itens da Biblioteca", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Selecionar Caminho da Pasta", "ButtonSeries": "Séries", "ButtonSetChaptersFromTracks": "Definir Capítulos Segundo Faixas", + "ButtonShare": "Share", "ButtonShiftTimes": "Deslocar tempos", "ButtonShow": "Exibir", "ButtonStartM4BEncode": "Iniciar Codificação M4B", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Atualizar Detalhes", "HeaderUpdateLibrary": "Atualizar Biblioteca", "HeaderUsers": "Usuários", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Suas Estatísticas", "LabelAbridged": "Versão Abreviada", "LabelAccountType": "Tipo de Conta", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Pode Fazer Download", "LabelPermissionsUpdate": "Pode Atualizar", "LabelPermissionsUpload": "Pode Fazer Upload", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Caminho/URL para Foto", "LabelPlaylists": "Listas de Reprodução", "LabelPlayMethod": "Método de Reprodução", @@ -436,6 +442,7 @@ "LabelSeries": "Série", "LabelSeriesName": "Nome da Série", "LabelSeriesProgress": "Progresso da Série", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Definir como principal", "LabelSetEbookAsSupplementary": "Definir como complementar", "LabelSettingsAudiobooksOnly": "Apenas Audiobooks", @@ -552,6 +559,8 @@ "LabelViewQueue": "Ver fila do reprodutor", "LabelVolume": "Volume", "LabelWeekdaysToRun": "Dias da semana para executar", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Duração do seu audiobook", "LabelYourBookmarks": "Seus Marcadores", "LabelYourPlaylists": "Suas Listas de Reprodução", @@ -765,4 +774,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} +} \ No newline at end of file diff --git a/client/strings/ru.json b/client/strings/ru.json index 3657b596..adc3b48b 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Найти всех авторов", "ButtonMatchBooks": "Найти книги", "ButtonNevermind": "Не важно", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "Ok", "ButtonOpenFeed": "Открыть канал", @@ -51,6 +52,7 @@ "ButtonPlay": "Слушать", "ButtonPlaying": "Проигрывается", "ButtonPlaylists": "Плейлисты", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Очистить весь кэш", "ButtonPurgeItemsCache": "Очистить кэш элементов", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Удалить из очереди", "ButtonQuickMatch": "Быстрый поиск", "ButtonRead": "Читать", + "ButtonRefresh": "Refresh", "ButtonRemove": "Удалить", "ButtonRemoveAll": "Удалить всё", "ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Выберите путь папки", "ButtonSeries": "Серии", "ButtonSetChaptersFromTracks": "Установить главы из треков", + "ButtonShare": "Share", "ButtonShiftTimes": "Смещение", "ButtonShow": "Показать", "ButtonStartM4BEncode": "Начать кодирование M4B", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Обновить детали", "HeaderUpdateLibrary": "Обновить библиотеку", "HeaderUsers": "Пользователи", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Ваша статистика", "LabelAbridged": "Сокращенное издание", "LabelAccountType": "Тип учетной записи", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Может скачивать", "LabelPermissionsUpdate": "Может обновлять", "LabelPermissionsUpload": "Может закачивать", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Путь к фото/URL", "LabelPlaylists": "Плейлисты", "LabelPlayMethod": "Метод воспроизведения", @@ -436,6 +442,7 @@ "LabelSeries": "Серия", "LabelSeriesName": "Имя серии", "LabelSeriesProgress": "Прогресс серии", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Установить как основную", "LabelSetEbookAsSupplementary": "Установить как дополнительную", "LabelSettingsAudiobooksOnly": "Только аудиокниги", @@ -552,6 +559,8 @@ "LabelViewQueue": "Очередь воспроизведения", "LabelVolume": "Громкость", "LabelWeekdaysToRun": "Дни недели для запуска", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Продолжительность Вашей книги", "LabelYourBookmarks": "Ваши закладки", "LabelYourPlaylists": "Ваши плейлисты", diff --git a/client/strings/sv.json b/client/strings/sv.json index 986f2c4b..9560e765 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "Matcha alla författare", "ButtonMatchBooks": "Matcha böcker", "ButtonNevermind": "Glöm det", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "Okej", "ButtonOpenFeed": "Öppna flöde", @@ -51,6 +52,7 @@ "ButtonPlay": "Spela", "ButtonPlaying": "Spelar", "ButtonPlaylists": "Spellistor", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Rensa all cache", "ButtonPurgeItemsCache": "Rensa föremåls-cache", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "Ta bort från kön", "ButtonQuickMatch": "Snabb matchning", "ButtonRead": "Läs", + "ButtonRefresh": "Refresh", "ButtonRemove": "Ta bort", "ButtonRemoveAll": "Ta bort alla", "ButtonRemoveAllLibraryItems": "Ta bort alla biblioteksobjekt", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "Välj mappens sökväg", "ButtonSeries": "Serie", "ButtonSetChaptersFromTracks": "Ställ in kapitel från spår", + "ButtonShare": "Share", "ButtonShiftTimes": "Förskjut tider", "ButtonShow": "Visa", "ButtonStartM4BEncode": "Starta M4B-kodning", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "Uppdatera detaljer", "HeaderUpdateLibrary": "Uppdatera bibliotek", "HeaderUsers": "Användare", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "Dina statistik", "LabelAbridged": "Förkortad", "LabelAccountType": "Kontotyp", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "Kan ladda ner", "LabelPermissionsUpdate": "Kan uppdatera", "LabelPermissionsUpload": "Kan ladda upp", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Bildsökväg/URL", "LabelPlaylists": "Spellistor", "LabelPlayMethod": "Spelläge", @@ -436,6 +442,7 @@ "LabelSeries": "Serie", "LabelSeriesName": "Serienamn", "LabelSeriesProgress": "Serieframsteg", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Ange som primär", "LabelSetEbookAsSupplementary": "Ange som kompletterande", "LabelSettingsAudiobooksOnly": "Endast ljudböcker", @@ -552,6 +559,8 @@ "LabelViewQueue": "Visa spellista", "LabelVolume": "Volym", "LabelWeekdaysToRun": "Vardagar att köra", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "Din ljudboks varaktighet", "LabelYourBookmarks": "Dina bokmärken", "LabelYourPlaylists": "Dina spellistor", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index fd7fe290..c1d9345f 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -43,6 +43,7 @@ "ButtonMatchAllAuthors": "匹配所有作者", "ButtonMatchBooks": "匹配图书", "ButtonNevermind": "没有关系", + "ButtonNext": "Next", "ButtonNextChapter": "Next Chapter", "ButtonOk": "确定", "ButtonOpenFeed": "打开源", @@ -51,6 +52,7 @@ "ButtonPlay": "播放", "ButtonPlaying": "正在播放", "ButtonPlaylists": "播放列表", + "ButtonPrevious": "Previous", "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "清理所有缓存", "ButtonPurgeItemsCache": "清理项目缓存", @@ -59,6 +61,7 @@ "ButtonQueueRemoveItem": "从队列中移除", "ButtonQuickMatch": "快速匹配", "ButtonRead": "读取", + "ButtonRefresh": "Refresh", "ButtonRemove": "移除", "ButtonRemoveAll": "移除所有", "ButtonRemoveAllLibraryItems": "移除所有媒体库项目", @@ -78,6 +81,7 @@ "ButtonSelectFolderPath": "选择文件夹路径", "ButtonSeries": "系列", "ButtonSetChaptersFromTracks": "将音轨设置为章节", + "ButtonShare": "Share", "ButtonShiftTimes": "快速调整时间", "ButtonShow": "显示", "ButtonStartM4BEncode": "开始 M4B 编码", @@ -180,6 +184,7 @@ "HeaderUpdateDetails": "更新详情", "HeaderUpdateLibrary": "更新媒体库", "HeaderUsers": "用户", + "HeaderYearReview": "Year {0} in Review", "HeaderYourStats": "你的统计数据", "LabelAbridged": "概要", "LabelAccountType": "帐户类型", @@ -391,6 +396,7 @@ "LabelPermissionsDownload": "可以下载", "LabelPermissionsUpdate": "可以更新", "LabelPermissionsUpload": "可以上传", + "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "图片路径或 URL", "LabelPlaylists": "播放列表", "LabelPlayMethod": "播放方法", @@ -436,6 +442,7 @@ "LabelSeries": "系列", "LabelSeriesName": "系列名称", "LabelSeriesProgress": "系列进度", + "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "设置为主", "LabelSetEbookAsSupplementary": "设置为补充", "LabelSettingsAudiobooksOnly": "只有有声读物", @@ -552,6 +559,8 @@ "LabelViewQueue": "查看播放列表", "LabelVolume": "音量", "LabelWeekdaysToRun": "工作日运行", + "LabelYearReviewHide": "Hide Year in Review", + "LabelYearReviewShow": "See Year in Review", "LabelYourAudiobookDuration": "你的有声读物持续时间", "LabelYourBookmarks": "你的书签", "LabelYourPlaylists": "你的播放列表", From c799379a5486e2999fb95b5a0c16fea877d91272 Mon Sep 17 00:00:00 2001 From: JBlond <leet31337@web.de> Date: Sat, 24 Feb 2024 20:23:51 +0100 Subject: [PATCH 0398/2145] Update de strings --- client/strings/de.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/strings/de.json b/client/strings/de.json index 46467734..5b2711c1 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -43,7 +43,7 @@ "ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)", "ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)", "ButtonNevermind": "Abbrechen", - "ButtonNext": "Next", + "ButtonNext": "Vor", "ButtonNextChapter": "Nächstes Kapitel", "ButtonOk": "Ok", "ButtonOpenFeed": "Feed öffnen", @@ -52,7 +52,7 @@ "ButtonPlay": "Abspielen", "ButtonPlaying": "Spielt", "ButtonPlaylists": "Wiedergabelisten", - "ButtonPrevious": "Previous", + "ButtonPrevious": "Zurück", "ButtonPreviousChapter": "Vorheriges Kapitel", "ButtonPurgeAllCache": "Cache leeren", "ButtonPurgeItemsCache": "Lösche Medien-Cache", @@ -292,11 +292,11 @@ "LabelFinished": "Beendet", "LabelFolder": "Ordner", "LabelFolders": "Verzeichnisse", - "LabelFontBold": "Bold", + "LabelFontBold": "Fett", "LabelFontFamily": "Schriftfamilie", - "LabelFontItalic": "Italic", + "LabelFontItalic": "Kursiv", "LabelFontScale": "Schriftgröße", - "LabelFontStrikethrough": "Strikethrough", + "LabelFontStrikethrough": "Durchgestrichen", "LabelFormat": "Format", "LabelGenre": "Kategorie", "LabelGenres": "Kategorien", @@ -419,7 +419,7 @@ "LabelRecentlyAdded": "Kürzlich hinzugefügt", "LabelRecentSeries": "Aktuelle Serien", "LabelRecommended": "Empfohlen", - "LabelRedo": "Redo", + "LabelRedo": "Wiederholen", "LabelRegion": "Region", "LabelReleaseDate": "Veröffentlichungsdatum", "LabelRemoveCover": "Lösche Titelbild", From e6735e042e8f61559cf35992d50cecbee7b30aee Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sun, 25 Feb 2024 09:01:26 +0200 Subject: [PATCH 0399/2145] Fix dup author addition logic --- server/models/LibraryItem.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index c7da31f6..49b44f9a 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -312,17 +312,18 @@ class LibraryItem extends Model { const existingAuthors = libraryItemExpanded.media.authors || [] const existingSeriesAll = libraryItemExpanded.media.series || [] const updatedAuthors = oldLibraryItem.media.metadata.authors || [] + const uniqueUpdatedAuthors = updatedAuthors.filter((au, idx) => updatedAuthors.findIndex(a => a.id === au.id) === idx) const updatedSeriesAll = oldLibraryItem.media.metadata.series || [] for (const existingAuthor of existingAuthors) { // Author was removed from Book - if (!updatedAuthors.some(au => au.id === existingAuthor.id)) { + if (!uniqueUpdatedAuthors.some(au => au.id === existingAuthor.id)) { Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`) await this.sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id) hasUpdates = true } } - for (const updatedAuthor of updatedAuthors) { + for (const updatedAuthor of uniqueUpdatedAuthors) { // Author was added if (!existingAuthors.some(au => au.id === updatedAuthor.id)) { Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`) From 3a99cc56b7a992793dd61ffb31e5a75ef575abcb Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 25 Feb 2024 12:56:04 -0600 Subject: [PATCH 0400/2145] Update:Debian packager script to use xz compression instead of zstd --- build/linuxpackager | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build/linuxpackager b/build/linuxpackager index a43d4ed1..5f03a2e8 100755 --- a/build/linuxpackager +++ b/build/linuxpackager @@ -50,9 +50,8 @@ echo "$controlfile" > dist/debian/DEBIAN/control; # Package debian pkg -t node18-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf . -fakeroot dpkg-deb --build dist/debian +fakeroot dpkg-deb -Zxz --build dist/debian mv dist/debian.deb "dist/$OUTPUT_FILE" -chmod +x "dist/$OUTPUT_FILE" echo "Finished! Filename: $OUTPUT_FILE" From b47793c365aaafee08db3cb4fe0b5a070029fb4f Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Mon, 26 Feb 2024 14:00:25 +0200 Subject: [PATCH 0401/2145] Add cache control header for timestamped cover image requests --- server/controllers/LibraryItemController.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index dfa1daea..3fa7743a 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -283,6 +283,9 @@ class LibraryItemController { return res.sendStatus(404) } + if (req.query.ts) + res.set('Cache-Control', 'private, max-age=86400') + if (raw) { // any value if (global.XAccel) { const encodedURI = encodeUriPath(global.XAccel + libraryItem.media.coverPath) From def2988e120b162da1f3e9df586813bccf156f63 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 26 Feb 2024 17:20:11 -0600 Subject: [PATCH 0402/2145] Update:Passport openid-client request timeout set to 10s (default was 3.5s) #2669 --- server/Auth.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/Auth.js b/server/Auth.js index cec3bc33..ad219dc2 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -76,6 +76,9 @@ class Auth { return } + // Custom req timeout see: https://github.com/panva/node-openid-client/blob/main/docs/README.md#customizing + OpenIDClient.custom.setHttpOptionsDefaults({ timeout: 10000 }) + const openIdIssuerClient = new OpenIDClient.Issuer({ issuer: global.ServerSettings.authOpenIDIssuerURL, authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL, From 72172dcb331f73864abccee1b6aed44416fd207e Mon Sep 17 00:00:00 2001 From: Machou <Machou@users.noreply.github.com> Date: Tue, 27 Feb 2024 08:56:31 +0100 Subject: [PATCH 0403/2145] Update fr.json --- client/strings/fr.json | 52 +++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index bbf38e18..81b6fa1b 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -7,7 +7,7 @@ "ButtonAddUser": "Ajouter un utilisateur", "ButtonAddYourFirstLibrary": "Ajouter votre première bibliothèque", "ButtonApply": "Appliquer", - "ButtonApplyChapters": "Appliquer les chapitres", + "ButtonApplyChapters": "Appliquer aux chapitres", "ButtonAuthors": "Auteurs", "ButtonBrowseForFolder": "Naviguer vers le répertoire", "ButtonCancel": "Annuler", @@ -32,8 +32,8 @@ "ButtonHide": "Cacher", "ButtonHome": "Accueil", "ButtonIssues": "Parutions", - "ButtonJumpBackward": "Jump Backward", - "ButtonJumpForward": "Jump Forward", + "ButtonJumpBackward": "Retour", + "ButtonJumpForward": "Avancer", "ButtonLatest": "Dernière version", "ButtonLibrary": "Bibliothèque", "ButtonLogout": "Me déconnecter", @@ -43,8 +43,8 @@ "ButtonMatchAllAuthors": "Chercher tous les auteurs", "ButtonMatchBooks": "Chercher les livres", "ButtonNevermind": "Non merci", - "ButtonNext": "Next", - "ButtonNextChapter": "Next Chapter", + "ButtonNext": "Suivant", + "ButtonNextChapter": "Chapitre suivant", "ButtonOk": "Ok", "ButtonOpenFeed": "Ouvrir le flux", "ButtonOpenManager": "Ouvrir le gestionnaire", @@ -52,8 +52,8 @@ "ButtonPlay": "Écouter", "ButtonPlaying": "En lecture", "ButtonPlaylists": "Listes de lecture", - "ButtonPrevious": "Previous", - "ButtonPreviousChapter": "Previous Chapter", + "ButtonPrevious": "Précédent", + "ButtonPreviousChapter": "Chapitre précédent", "ButtonPurgeAllCache": "Purger le cache", "ButtonPurgeItemsCache": "Purger le cache des articles", "ButtonPurgeMediaProgress": "Purger la progression des médias", @@ -61,7 +61,7 @@ "ButtonQueueRemoveItem": "Supprimer de la liste de lecture", "ButtonQuickMatch": "Recherche rapide", "ButtonRead": "Lire", - "ButtonRefresh": "Refresh", + "ButtonRefresh": "Rafraîchir", "ButtonRemove": "Supprimer", "ButtonRemoveAll": "Supprimer tout", "ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque", @@ -81,7 +81,7 @@ "ButtonSelectFolderPath": "Sélectionner le chemin du dossier", "ButtonSeries": "Séries", "ButtonSetChaptersFromTracks": "Positionner les chapitres par rapports aux pistes", - "ButtonShare": "Share", + "ButtonShare": "Partager", "ButtonShiftTimes": "Décaler l’horodatage du livre", "ButtonShow": "Afficher", "ButtonStartM4BEncode": "Démarrer l’encodage M4B", @@ -92,7 +92,7 @@ "ButtonUploadBackup": "Téléverser une sauvegarde", "ButtonUploadCover": "Téléverser une couverture", "ButtonUploadOPMLFile": "Téléverser un fichier OPML", - "ButtonUserDelete": "Effacer l’utilisateur {0}", + "ButtonUserDelete": "Supprimer l’utilisateur {0}", "ButtonUserEdit": "Modifier l’utilisateur {0}", "ButtonViewAll": "Afficher tout", "ButtonYes": "Oui", @@ -101,8 +101,8 @@ "ErrorUploadLacksTitle": "Doit avoir un titre", "HeaderAccount": "Compte", "HeaderAdvanced": "Avancé", - "HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise", - "HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook", + "HeaderAppriseNotificationSettings": "Configuration des notifications Apprise", + "HeaderAudiobookTools": "Outils de gestion de fichiers de livres audio", "HeaderAudioTracks": "Pistes audio", "HeaderAuthentication": "Authentication", "HeaderBackups": "Sauvegardes", @@ -113,7 +113,7 @@ "HeaderCollectionItems": "Entrées de la collection", "HeaderCover": "Couverture", "HeaderCurrentDownloads": "Téléchargements en cours", - "HeaderCustomMetadataProviders": "Custom Metadata Providers", + "HeaderCustomMetadataProviders": "Fournisseurs de métadonnées personnalisés", "HeaderDetails": "Détails", "HeaderDownloadQueue": "File d’attente de téléchargements", "HeaderEbookFiles": "Fichier des livres numériques", @@ -292,11 +292,11 @@ "LabelFinished": "Terminé le", "LabelFolder": "Dossier", "LabelFolders": "Dossiers", - "LabelFontBold": "Bold", + "LabelFontBold": "Gras", "LabelFontFamily": "Polices de caractères", - "LabelFontItalic": "Italic", + "LabelFontItalic": "Italique", "LabelFontScale": "Taille de la police de caractère", - "LabelFontStrikethrough": "Strikethrough", + "LabelFontStrikethrough": "Barrer", "LabelFormat": "Format", "LabelGenre": "Genre", "LabelGenres": "Genres", @@ -419,7 +419,7 @@ "LabelRecentlyAdded": "Derniers ajouts", "LabelRecentSeries": "Séries récentes", "LabelRecommended": "Recommandé", - "LabelRedo": "Redo", + "LabelRedo": "Refaire", "LabelRegion": "Région", "LabelReleaseDate": "Date de parution", "LabelRemoveCover": "Supprimer la couverture", @@ -509,10 +509,10 @@ "LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur", "LabelTagsNotAccessibleToUser": "Étiquettes non accessibles à l’utilisateur", "LabelTasks": "Tâches en cours", - "LabelTextEditorBulletedList": "Bulleted list", - "LabelTextEditorLink": "Link", - "LabelTextEditorNumberedList": "Numbered list", - "LabelTextEditorUnlink": "Unlink", + "LabelTextEditorBulletedList": "Liste à puces", + "LabelTextEditorLink": "Lien", + "LabelTextEditorNumberedList": "Liste numérotée", + "LabelTextEditorUnlink": "Dissocier", "LabelTheme": "Thème", "LabelThemeDark": "Sombre", "LabelThemeLight": "Clair", @@ -538,7 +538,7 @@ "LabelTracksSingleTrack": "Piste simple", "LabelType": "Type", "LabelUnabridged": "Version intégrale", - "LabelUndo": "Undo", + "LabelUndo": "Annuler", "LabelUnknown": "Inconnu", "LabelUpdateCover": "Mettre à jour la couverture", "LabelUpdateCoverHelp": "Autoriser la mise à jour de la couverture existante lorsqu’une correspondance est trouvée", @@ -559,8 +559,8 @@ "LabelViewQueue": "Afficher la liste de lecture", "LabelVolume": "Volume", "LabelWeekdaysToRun": "Jours de la semaine à exécuter", - "LabelYearReviewHide": "Hide Year in Review", - "LabelYearReviewShow": "See Year in Review", + "LabelYearReviewHide": "Masquer le bilan de l’année", + "LabelYearReviewShow": "Afficher le bilan de l’année", "LabelYourAudiobookDuration": "Durée de vos livres audios", "LabelYourBookmarks": "Vos signets", "LabelYourPlaylists": "Vos listes de lecture", @@ -692,7 +692,7 @@ "MessageYourAudiobookDurationIsShorter": "La durée de votre livre audio est plus courte que la durée trouvée", "NoteChangeRootPassword": "seul l’utilisateur « root » peut utiliser un mot de passe vide", "NoteChapterEditorTimes": "Information : l’horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du livre audio.", - "NoteFolderPicker": "Information : Les dossiers déjà surveillés ne sont pas affichés", + "NoteFolderPicker": "Information : les dossiers déjà surveillés ne sont pas affichés", "NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.", "NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.", "NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.", @@ -774,4 +774,4 @@ "ToastSocketFailedToConnect": "Échec de la connexion WebSocket", "ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur", "ToastUserDeleteSuccess": "Utilisateur supprimé" -} \ No newline at end of file +} From 655bebfec4c6ba71751a7aa4a0f3e7af7f5e4789 Mon Sep 17 00:00:00 2001 From: Teekeks <info@teawork.de> Date: Tue, 27 Feb 2024 18:30:05 +0100 Subject: [PATCH 0404/2145] feat: Expanded filter to include "has no ebook" and "has no supplementary ebooks" options --- client/components/controls/LibraryFilterSelect.vue | 8 ++++++++ client/strings/de.json | 2 ++ client/strings/en-us.json | 2 ++ server/utils/queries/libraryItemsBookFilters.js | 8 ++++++++ 4 files changed, 20 insertions(+) diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue index 5ed4400b..04dd11af 100644 --- a/client/components/controls/LibraryFilterSelect.vue +++ b/client/components/controls/LibraryFilterSelect.vue @@ -368,9 +368,17 @@ export default { id: 'ebook', name: this.$strings.LabelHasEbook }, + { + id: 'no-ebook', + name: this.$strings.LabelMissingEbook + }, { id: 'supplementary', name: this.$strings.LabelHasSupplementaryEbook + }, + { + id: 'no-supplementary', + name: this.$strings.LabelMissingSupplementaryEbook } ] }, diff --git a/client/strings/de.json b/client/strings/de.json index 5b2711c1..33b0fb1b 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta Tags", "LabelMinute": "Minute", "LabelMissing": "Fehlend", + "LabelMissingEbook": "E-Book fehlt", "LabelMissingParts": "Fehlende Teile", + "LabelMissingSupplementaryEbook": "Ergänzendes E-Book fehlt", "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 du entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen kannst. Die Verwendung eines Sternchens (<code>*</code>) als alleiniger Eintrag erlaubt jede URI.", "LabelMore": "Mehr", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index b8860ffa..2465f873 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta Tags", "LabelMinute": "Minute", "LabelMissing": "Missing", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Missing Parts", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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", diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index d23459b4..16a25847 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -204,6 +204,10 @@ module.exports = { mediaWhere['ebookFile'] = { [Sequelize.Op.not]: null } + } else if (value == 'no-ebook') { + mediaWhere['ebookFile'] = { + [Sequelize.Op.eq]: null + } } } else if (group === 'missing') { if (['asin', 'isbn', 'subtitle', 'publishedYear', 'description', 'publisher', 'language', 'cover'].includes(value)) { @@ -421,6 +425,10 @@ module.exports = { libraryItemWhere['libraryFiles'] = { [Sequelize.Op.substring]: `"isSupplementary":true` } + } else if (filterGroup === 'ebooks' && filterValue === 'no-supplementary') { + libraryItemWhere['libraryFiles'] = { + [Sequelize.Op.notLike]: Sequelize.literal(`\'%"isSupplementary":true%\'`), + } } else if (filterGroup === 'missing' && filterValue === 'authors') { authorInclude = { model: Database.authorModel, From 20ec54e085ef0d7491ddfe0b40bbcc50a2ba4852 Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:31:21 -0300 Subject: [PATCH 0405/2145] [PT-BR] Updated strings --- client/strings/pt-br.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 14e7f52a..1d88c209 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -43,7 +43,7 @@ "ButtonMatchAllAuthors": "Consultar Todos os Autores", "ButtonMatchBooks": "Consultar Livros", "ButtonNevermind": "Cancelar", - "ButtonNext": "Next", + "ButtonNext": "Próximo", "ButtonNextChapter": "Próximo Capítulo", "ButtonOk": "Ok", "ButtonOpenFeed": "Abrir Feed", @@ -52,7 +52,7 @@ "ButtonPlay": "Reproduzir", "ButtonPlaying": "Reproduzindo", "ButtonPlaylists": "Lista de Reprodução", - "ButtonPrevious": "Previous", + "ButtonPrevious": "Anterior", "ButtonPreviousChapter": "Capítulo Anterior", "ButtonPurgeAllCache": "Apagar Todo o Cache", "ButtonPurgeItemsCache": "Apagar o Cache de Itens", @@ -61,7 +61,7 @@ "ButtonQueueRemoveItem": "Remover da Lista", "ButtonQuickMatch": "Consulta rápida", "ButtonRead": "Ler", - "ButtonRefresh": "Refresh", + "ButtonRefresh": "Atualizar", "ButtonRemove": "Remover", "ButtonRemoveAll": "Remover Todos", "ButtonRemoveAllLibraryItems": "Remover Todos os Itens da Biblioteca", @@ -81,7 +81,7 @@ "ButtonSelectFolderPath": "Selecionar Caminho da Pasta", "ButtonSeries": "Séries", "ButtonSetChaptersFromTracks": "Definir Capítulos Segundo Faixas", - "ButtonShare": "Share", + "ButtonShare": "Compartilhar", "ButtonShiftTimes": "Deslocar tempos", "ButtonShow": "Exibir", "ButtonStartM4BEncode": "Iniciar Codificação M4B", @@ -184,7 +184,7 @@ "HeaderUpdateDetails": "Atualizar Detalhes", "HeaderUpdateLibrary": "Atualizar Biblioteca", "HeaderUsers": "Usuários", - "HeaderYearReview": "Year {0} in Review", + "HeaderYearReview": "Retrospectiva de {0} ", "HeaderYourStats": "Suas Estatísticas", "LabelAbridged": "Versão Abreviada", "LabelAccountType": "Tipo de Conta", @@ -396,13 +396,13 @@ "LabelPermissionsDownload": "Pode Fazer Download", "LabelPermissionsUpdate": "Pode Atualizar", "LabelPermissionsUpload": "Pode Fazer Upload", - "LabelPersonalYearReview": "Your Year in Review ({0})", + "LabelPersonalYearReview": "Sua Retrospectiva Anual ({0})", "LabelPhotoPathURL": "Caminho/URL para Foto", "LabelPlaylists": "Listas de Reprodução", "LabelPlayMethod": "Método de Reprodução", "LabelPodcast": "Podcast", "LabelPodcasts": "Podcasts", - "LabelPodcastSearchRegion": "Podcast search region", + "LabelPodcastSearchRegion": "Região de busca do podcast", "LabelPodcastType": "Tipo de Podcast", "LabelPort": "Porta", "LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)", @@ -442,7 +442,7 @@ "LabelSeries": "Série", "LabelSeriesName": "Nome da Série", "LabelSeriesProgress": "Progresso da Série", - "LabelServerYearReview": "Server Year in Review ({0})", + "LabelServerYearReview": "Retrospectiva Anual do Servidor ({0})", "LabelSetEbookAsPrimary": "Definir como principal", "LabelSetEbookAsSupplementary": "Definir como complementar", "LabelSettingsAudiobooksOnly": "Apenas Audiobooks", @@ -559,8 +559,8 @@ "LabelViewQueue": "Ver fila do reprodutor", "LabelVolume": "Volume", "LabelWeekdaysToRun": "Dias da semana para executar", - "LabelYearReviewHide": "Hide Year in Review", - "LabelYearReviewShow": "See Year in Review", + "LabelYearReviewHide": "Ocultar Retrospectiva Anual", + "LabelYearReviewShow": "Exibir Retrospectiva Anual", "LabelYourAudiobookDuration": "Duração do seu audiobook", "LabelYourBookmarks": "Seus Marcadores", "LabelYourPlaylists": "Suas Listas de Reprodução", @@ -774,4 +774,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} \ No newline at end of file +} From 38f12f47950713557a92def0f6871fef2482c7cc Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 27 Feb 2024 17:17:33 -0600 Subject: [PATCH 0406/2145] Fix:Podcast schedule max new episodes to download setting to 0 and fix input blurs #2680 --- client/components/modals/item/tabs/Schedule.vue | 11 ++++++++--- server/objects/mediaTypes/Podcast.js | 8 +++++++- server/utils/index.js | 4 +++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/client/components/modals/item/tabs/Schedule.vue b/client/components/modals/item/tabs/Schedule.vue index 032936d1..819ede19 100644 --- a/client/components/modals/item/tabs/Schedule.vue +++ b/client/components/modals/item/tabs/Schedule.vue @@ -20,7 +20,7 @@ </ui-tooltip> </div> <div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2"> - <ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" /> + <ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" /> <ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded."> <p class="pl-4 text-base"> Max new episodes to download per check @@ -129,9 +129,12 @@ export default { return } } - if (this.$refs.maxEpisodesInput && this.$refs.maxEpisodesInput.isFocused) { + + if (this.$refs.maxEpisodesInput?.isFocused) { this.$refs.maxEpisodesInput.blur() - return + } + if (this.$refs.maxEpisodesToDownloadInput?.isFocused) { + this.$refs.maxEpisodesToDownloadInput.blur() } const updatePayload = { @@ -140,9 +143,11 @@ export default { if (this.enableAutoDownloadEpisodes) { updatePayload.autoDownloadSchedule = this.cronExpression } + this.newMaxEpisodesToKeep = Number(this.newMaxEpisodesToKeep) if (this.newMaxEpisodesToKeep !== this.maxEpisodesToKeep) { updatePayload.maxEpisodesToKeep = this.newMaxEpisodesToKeep } + this.newMaxNewEpisodesToDownload = Number(this.newMaxNewEpisodesToDownload) if (this.newMaxNewEpisodesToDownload !== this.maxNewEpisodesToDownload) { updatePayload.maxNewEpisodesToDownload = this.newMaxNewEpisodesToDownload } diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js index a0e5de04..6f7ca337 100644 --- a/server/objects/mediaTypes/Podcast.js +++ b/server/objects/mediaTypes/Podcast.js @@ -42,7 +42,13 @@ class Podcast { this.autoDownloadSchedule = podcast.autoDownloadSchedule || '0 * * * *' // Added in 2.1.3 so default to hourly this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0 this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0 - this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload || 3 + + // Default is 3 but 0 is allowed + if (typeof podcast.maxNewEpisodesToDownload !== 'number') { + this.maxNewEpisodesToDownload = 3 + } else { + this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload + } } toJSON() { diff --git a/server/utils/index.js b/server/utils/index.js index f75572ba..6a89621b 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -114,7 +114,9 @@ module.exports.reqSupportsWebp = (req) => { module.exports.areEquivalent = areEquivalent module.exports.copyValue = (val) => { - if (!val) return val === false ? false : null + if (val === undefined || val === '') return null + else if (!val) return val + if (!this.isObject(val)) return val if (Array.isArray(val)) { From f4a19e48ad21241c27f0b0f4c07cf3d758f37b45 Mon Sep 17 00:00:00 2001 From: mfcar <marcosfcar@gmail.com> Date: Wed, 28 Feb 2024 19:21:11 +0000 Subject: [PATCH 0407/2145] Update login page --- client/pages/login.vue | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 9b7758d8..efdbba4d 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -1,5 +1,5 @@ <template> - <div class="w-full h-screen bg-bg"> + <div id="page-wrapper" class="w-full h-screen"> <div class="w-full flex h-full items-center justify-center"> <div v-if="criticalError" class="w-full max-w-md rounded border border-error border-opacity-25 bg-error bg-opacity-10 p-4"> <p class="text-center text-lg font-semibold">{{ $strings.MessageServerCouldNotBeReached }}</p> @@ -24,9 +24,10 @@ </form> </div> <div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40"> + <div class="flex justify-center mb-4"><img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-32 min-w-32 h-32" /></div> <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" /> + <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> <p v-if="loginCustomMessage" class="py-2 default-style mb-2" v-html="loginCustomMessage"></p> @@ -50,6 +51,7 @@ {{ openIDButtonText }} </a> </div> + </div> </div> </div> </div> @@ -281,4 +283,4 @@ export default { this.checkStatus() } } -</script> \ No newline at end of file +</script> From d2b006b909f2b3ab2569e3929f8018aeb342fe75 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 28 Feb 2024 16:16:26 -0600 Subject: [PATCH 0408/2145] Update:Windows binary manager to install ffmpeg/ffprobe 5.1 #1098 --- server/managers/BinaryManager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/managers/BinaryManager.js b/server/managers/BinaryManager.js index ec4ed3b6..b4121ab8 100644 --- a/server/managers/BinaryManager.js +++ b/server/managers/BinaryManager.js @@ -11,8 +11,8 @@ const fileUtils = require('../utils/fileUtils') class BinaryManager { defaultRequiredBinaries = [ - { name: 'ffmpeg', envVariable: 'FFMPEG_PATH', validVersions: ['5.1', '6'] }, - { name: 'ffprobe', envVariable: 'FFPROBE_PATH', validVersions: ['5.1', '6'] } + { name: 'ffmpeg', envVariable: 'FFMPEG_PATH', validVersions: ['5.1'] }, + { name: 'ffprobe', envVariable: 'FFPROBE_PATH', validVersions: ['5.1'] } ] constructor(requiredBinaries = this.defaultRequiredBinaries) { @@ -135,7 +135,7 @@ class BinaryManager { if (!binaries.length) return Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`) let destination = await fileUtils.isWritable(this.mainInstallPath) ? this.mainInstallPath : this.altInstallPath - await ffbinaries.downloadBinaries(binaries, { destination, version: '6.1', force: true }) + await ffbinaries.downloadBinaries(binaries, { destination, version: '5.1', force: true }) Logger.info(`[BinaryManager] Binaries installed to ${destination}`) } From 987842ed04e5a7e8025bdfc2ed576d9569eed8ec Mon Sep 17 00:00:00 2001 From: Lars Kiesow <lkiesow@uos.de> Date: Thu, 29 Feb 2024 17:56:55 +0100 Subject: [PATCH 0409/2145] Fix file names with URL control characters This patch ensures that files names like `series #3 xy.jpg` are actually handled correctly instead of the part after `#` being interpreted as fragment and being discarded. I noticed that in a few rare cases the App wouldn't properly display cover images. It turns out that due the file names containing a `#`, the file path got corrupted, causing Audiobookshelf to return a 403. --- server/utils/fileUtils.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 99bb49eb..1fc161c7 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -357,7 +357,10 @@ module.exports.removeFile = (path) => { } module.exports.encodeUriPath = (path) => { - const uri = new URL(path, "file://") + const uri = new URL('/', "file://") + // we assign the path here to assure that URL control characters like # are + // actually interpreted as part of the URL path + uri.pathname = path return uri.pathname } From 79d32274aa5c5a5cc02975e3dbc3b36fdebdb3e6 Mon Sep 17 00:00:00 2001 From: Lars Kiesow <lkiesow@uos.de> Date: Thu, 29 Feb 2024 18:16:29 +0100 Subject: [PATCH 0410/2145] Fix log source in log file The logger should include a source containing the location where the logger was called. This works well for logging to `stdout`. Unfortunately, the file logs contain the locations where the file logging is called inside of the logger. This is not helpful: ``` {"timestamp":"2023-11-19 16:35:43","source":"Logger.js:114","message":"[oldDbFiles] Processed db data file with 1 entities","levelName":"INFO","level":2} {"timestamp":"2023-11-19 16:35:43","source":"Logger.js:114","message":"[oldDbFiles] Finished loading db data with 2 entities","levelName":"INFO","level":2} {"timestamp":"2023-11-19 16:35:43","source":"Logger.js:114","message":"[oldDbFiles] 2 settings loaded","levelName":"INFO","level":2} ``` This patch fixes the issue, ensureing that the actual source location will be logged: ``` {"timestamp":"2024-02-29 18:12:59.832","source":"DailyLog.js:132","message":"[DailyLog] 2024-02-29: Loaded 20 Logs","levelName":"DEBUG","level":1} {"timestamp":"2024-02-29 18:12:59.638","source":"Server.js:172","message":"=== Starting Server ===","levelName":"INFO","level":2} {"timestamp":"2024-02-29 18:12:59.638","source":"Server.js:103","message":"[Server] Init v2.8.0","levelName":"INFO","level":2} ``` --- server/Logger.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/server/Logger.js b/server/Logger.js index ba20801f..7cc7aa4c 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -70,14 +70,15 @@ class Logger { } /** - * - * @param {number} level - * @param {string[]} args + * + * @param {number} level + * @param {string[]} args + * @param {string} src */ - async handleLog(level, args) { + async handleLog(level, args, src) { const logObj = { timestamp: this.timestamp, - source: this.source, + source: src, message: args.join(' '), levelName: this.getLogLevelString(level), level @@ -104,31 +105,31 @@ class Logger { trace(...args) { if (this.logLevel > LogLevel.TRACE) return console.trace(`[${this.timestamp}] TRACE:`, ...args) - this.handleLog(LogLevel.TRACE, args) + this.handleLog(LogLevel.TRACE, args, this.source) } debug(...args) { if (this.logLevel > LogLevel.DEBUG) return console.debug(`[${this.timestamp}] DEBUG:`, ...args, `(${this.source})`) - this.handleLog(LogLevel.DEBUG, args) + this.handleLog(LogLevel.DEBUG, args, this.source) } info(...args) { if (this.logLevel > LogLevel.INFO) return console.info(`[${this.timestamp}] INFO:`, ...args) - this.handleLog(LogLevel.INFO, args) + this.handleLog(LogLevel.INFO, args, this.source) } warn(...args) { if (this.logLevel > LogLevel.WARN) return console.warn(`[${this.timestamp}] WARN:`, ...args, `(${this.source})`) - this.handleLog(LogLevel.WARN, args) + this.handleLog(LogLevel.WARN, args, this.source) } error(...args) { if (this.logLevel > LogLevel.ERROR) return console.error(`[${this.timestamp}] ERROR:`, ...args, `(${this.source})`) - this.handleLog(LogLevel.ERROR, args) + this.handleLog(LogLevel.ERROR, args, this.source) } /** @@ -139,12 +140,12 @@ class Logger { */ fatal(...args) { console.error(`[${this.timestamp}] FATAL:`, ...args, `(${this.source})`) - return this.handleLog(LogLevel.FATAL, args) + return this.handleLog(LogLevel.FATAL, args, this.source) } note(...args) { console.log(`[${this.timestamp}] NOTE:`, ...args) - this.handleLog(LogLevel.NOTE, args) + this.handleLog(LogLevel.NOTE, args, this.source) } } module.exports = new Logger() \ No newline at end of file From 763bb1b8293fe1ca1e17336f4cdb5c5d506d4483 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 29 Feb 2024 13:59:00 -0600 Subject: [PATCH 0411/2145] Map ebook filter translations --- client/strings/cs.json | 2 ++ client/strings/da.json | 2 ++ client/strings/es.json | 2 ++ client/strings/et.json | 2 ++ client/strings/fr.json | 2 ++ client/strings/gu.json | 2 ++ client/strings/hi.json | 2 ++ client/strings/hr.json | 2 ++ client/strings/hu.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/pt-br.json | 2 ++ client/strings/ru.json | 2 ++ client/strings/sv.json | 2 ++ client/strings/zh-cn.json | 2 ++ 18 files changed, 36 insertions(+) diff --git a/client/strings/cs.json b/client/strings/cs.json index 3134d60d..7dc18aa6 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Metaznačky", "LabelMinute": "Minuta", "LabelMissing": "Chybějící", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Chybějící díly", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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", diff --git a/client/strings/da.json b/client/strings/da.json index f79f19b1..8c305ca2 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta-tags", "LabelMinute": "Minut", "LabelMissing": "Mangler", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Manglende dele", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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", diff --git a/client/strings/es.json b/client/strings/es.json index 75052413..452b625d 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Metaetiquetas", "LabelMinute": "Minuto", "LabelMissing": "Ausente", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Partes Ausentes", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos", "LabelMobileRedirectURIsDescription": "Esta es una lista de URIs válidos para redireccionamiento de apps móviles. La URI por defecto es <code>audiobookshelf://oauth</code>, la cual puedes remover or corroborar con URIs adicionales para la integración con apps de terceros. Utilizando un asterisco (<code>*</code>) como el único punto de entrada permite cualquier URI.", "LabelMore": "Más", diff --git a/client/strings/et.json b/client/strings/et.json index 07f9eef0..8c13c435 100644 --- a/client/strings/et.json +++ b/client/strings/et.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta märgendid", "LabelMinute": "Minut", "LabelMissing": "Puudub", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Puuduvad osad", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Lubatud mobiilile suunamise URI-d", "LabelMobileRedirectURIsDescription": "See on mobiilirakenduste jaoks kehtivate suunamise URI-de lubatud nimekiri. Vaikimisi on selleks <code>audiobookshelf://oauth</code>, mida saate eemaldada või täiendada täiendavate URI-dega kolmanda osapoole rakenduste integreerimiseks. Tärni (<code>*</code>) ainukese kirjena kasutamine võimaldab mis tahes URI-d.", "LabelMore": "Rohkem", diff --git a/client/strings/fr.json b/client/strings/fr.json index bbf38e18..d855c366 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Balises de métadonnée", "LabelMinute": "Minute", "LabelMissing": "Manquant", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Parties manquantes", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "URI de redirection mobile autorisés", "LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche d’URI de redirection valides pour les applications mobiles. Celui par défaut est <code>audiobookshelf://oauth</code>, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l'intégration d'applications tierces. L’utilisation d’un astérisque (<code>*</code>) comme seule entrée autorise n’importe quel URI.", "LabelMore": "Plus", diff --git a/client/strings/gu.json b/client/strings/gu.json index 2f3d9cd8..eff18343 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta Tags", "LabelMinute": "Minute", "LabelMissing": "Missing", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Missing Parts", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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", diff --git a/client/strings/hi.json b/client/strings/hi.json index 01667b59..d126e9f5 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta Tags", "LabelMinute": "Minute", "LabelMissing": "Missing", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Missing Parts", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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", diff --git a/client/strings/hr.json b/client/strings/hr.json index 8aa933c7..313505ac 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta Tags", "LabelMinute": "Minuta", "LabelMissing": "Nedostaje", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Nedostajali dijelovi", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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", diff --git a/client/strings/hu.json b/client/strings/hu.json index d52957e1..1829916a 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta címkék", "LabelMinute": "Perc", "LabelMissing": "Hiányzó", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Hiányzó részek", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Engedélyezett mobil átirányítási URI-k", "LabelMobileRedirectURIsDescription": "Ez egy fehérlista az érvényes mobilalkalmazás-átirányítási URI-k számára. Az alapértelmezett <code>audiobookshelf://oauth</code>, amely eltávolítható vagy kiegészíthető további URI-kkal harmadik féltől származó alkalmazásintegráció érdekében. Ha az egyetlen bejegyzés egy csillag (<code>*</code>), akkor bármely URI engedélyezett.", "LabelMore": "Több", diff --git a/client/strings/it.json b/client/strings/it.json index 26a61fda..f4e97048 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta Tags", "LabelMinute": "Minuto", "LabelMissing": "Altro", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Parti rimanenti", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti", "LabelMobileRedirectURIsDescription": "Questa è una lista bianca di URI di reindirizzamento validi per le app mobili. Quello predefinito è <code>audiobookshelf://oauth</code>, che puoi rimuovere o integrare con URI aggiuntivi per l'integrazione di app di terze parti. Utilizzando un asterisco (<code>*</code>) poiché l'unica voce consente qualsiasi URI.", "LabelMore": "Molto", diff --git a/client/strings/lt.json b/client/strings/lt.json index 1c2558c9..4d97ab47 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta žymos", "LabelMinute": "Minutė", "LabelMissing": "Trūksta", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Trūkstamos dalys", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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", diff --git a/client/strings/nl.json b/client/strings/nl.json index dd06146d..858b3de6 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta-tags", "LabelMinute": "Minuut", "LabelMissing": "Ontbrekend", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Ontbrekende delen", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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", diff --git a/client/strings/no.json b/client/strings/no.json index a29a7ef2..065d28df 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta Tags", "LabelMinute": "Minutt", "LabelMissing": "Mangler", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Manglende deler", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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", diff --git a/client/strings/pl.json b/client/strings/pl.json index 3189ccb7..2d4141ba 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Meta Tags", "LabelMinute": "Minuta", "LabelMissing": "Brakujący", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Brakujące cześci", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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", diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 14e7f52a..887b05b9 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Etiquetas Meta", "LabelMinute": "Minuto", "LabelMissing": "Ausente", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Partes Ausentes", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "URIs de redirecionamento móveis permitidas", "LabelMobileRedirectURIsDescription": "Essa é uma lista de permissionamento para URIs válidas para o redirecionamento de aplicativos móveis. A padrão é <code>audiobookshelf://oauth</code>, que pode ser removida ou acrescentada com novas URIs para integração com apps de terceiros. Usando um asterisco (<code>*</code>) como um item único dará permissão para qualquer URI.", "LabelMore": "Mais", diff --git a/client/strings/ru.json b/client/strings/ru.json index adc3b48b..6d690ee6 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Мета теги", "LabelMinute": "Минуты", "LabelMissing": "Потеряно", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Потерянные части", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств", "LabelMobileRedirectURIsDescription": "Это белый список допустимых URI перенаправления для мобильных приложений. По умолчанию используется <code>audiobookshelf://oauth</code>, который можно удалить или дополнить дополнительными URI для интеграции со сторонними приложениями. Использование звездочки (<code>*</code>) в качестве единственной записи разрешает любой URI.", "LabelMore": "Еще", diff --git a/client/strings/sv.json b/client/strings/sv.json index 9560e765..55e92be4 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -356,7 +356,9 @@ "LabelMetaTags": "Metamärken", "LabelMinute": "Minut", "LabelMissing": "Saknad", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "Saknade delar", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index c1d9345f..0634c74f 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -356,7 +356,9 @@ "LabelMetaTags": "元标签", "LabelMinute": "分钟", "LabelMissing": "丢失", + "LabelMissingEbook": "Has no ebook", "LabelMissingParts": "丢失的部分", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "允许移动应用重定向 URI", "LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 <code>audiobookshelf://oauth</code>,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (<code>*</code>) 作为唯一条目允许任何 URI.", "LabelMore": "更多", From 4fe672f09d794c05d31a9375b2a686ed1c0dfe10 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Fri, 1 Mar 2024 11:55:53 +0200 Subject: [PATCH 0412/2145] Update cover image URLs with timestamp where available --- client/components/modals/item/tabs/Match.vue | 4 ++-- client/pages/library/_library/podcast/latest.vue | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 7051a444..72aa7116 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -49,8 +49,8 @@ </div> <div v-if="media.coverPath"> <p class="text-center text-gray-200">Current</p> - <a :href="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" target="_blank" class="bg-primary"> - <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" /> + <a :href="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" target="_blank" class="bg-primary"> + <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" /> </a> </div> </div> diff --git a/client/pages/library/_library/podcast/latest.vue b/client/pages/library/_library/podcast/latest.vue index 42f107c8..3fc47dfd 100644 --- a/client/pages/library/_library/podcast/latest.vue +++ b/client/pages/library/_library/podcast/latest.vue @@ -8,11 +8,11 @@ <p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">{{ $strings.MessageNoEpisodes }}</p> <template v-for="(episode, index) in episodesMapped"> <div :key="episode.id" class="flex py-5 cursor-pointer relative" @click.stop="clickEpisode(episode)"> - <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" /> + <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId, episode.updatedAt)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" /> <div class="flex-grow pl-4 max-w-2xl"> <!-- mobile --> <div class="flex md:hidden mb-2"> - <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" /> + <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId, episode.updatedAt)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" /> <div class="flex-grow px-2"> <div class="flex items-center"> <div class="flex" @click.stop> From b52341dbcfdbc480120fd4c73d39dab14d4aec46 Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Sun, 3 Mar 2024 13:32:01 -0300 Subject: [PATCH 0413/2145] [PT-BR] enhance-ebook-filter strings translation --- client/strings/pt-br.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 2630bc09..0f0b0f06 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -356,9 +356,9 @@ "LabelMetaTags": "Etiquetas Meta", "LabelMinute": "Minuto", "LabelMissing": "Ausente", - "LabelMissingEbook": "Has no ebook", + "LabelMissingEbook": "Ebook não existe", "LabelMissingParts": "Partes Ausentes", - "LabelMissingSupplementaryEbook": "Has no supplementary ebook", + "LabelMissingSupplementaryEbook": "Ebook complementar não existe", "LabelMobileRedirectURIs": "URIs de redirecionamento móveis permitidas", "LabelMobileRedirectURIsDescription": "Essa é uma lista de permissionamento para URIs válidas para o redirecionamento de aplicativos móveis. A padrão é <code>audiobookshelf://oauth</code>, que pode ser removida ou acrescentada com novas URIs para integração com apps de terceiros. Usando um asterisco (<code>*</code>) como um item único dará permissão para qualquer URI.", "LabelMore": "Mais", From 5005aabe5ec36b5d2123aff16c08bee3699c5593 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sun, 3 Mar 2024 23:40:47 +0200 Subject: [PATCH 0414/2145] Fix input width in MultiSelect components --- client/components/ui/MultiSelect.vue | 11 +---------- client/components/ui/MultiSelectQueryInput.vue | 11 +---------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue index 2009b28d..3644f4ab 100644 --- a/client/components/ui/MultiSelect.vue +++ b/client/components/ui/MultiSelect.vue @@ -11,7 +11,7 @@ </div> {{ item }} </div> - <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> + <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: fit-content" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> </div> </form> @@ -110,15 +110,6 @@ export default { this.typingTimeout = setTimeout(() => { this.currentSearch = this.textInput }, 100) - this.setInputWidth() - }, - setInputWidth() { - setTimeout(() => { - var value = this.$refs.input.value - var len = value.length * 7 + 24 - this.$refs.input.style.width = len + 'px' - this.recalcMenuPos() - }, 50) }, recalcMenuPos() { if (!this.menu || !this.$refs.inputWrapper) return diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue index c86d3228..54de7584 100644 --- a/client/components/ui/MultiSelectQueryInput.vue +++ b/client/components/ui/MultiSelectQueryInput.vue @@ -14,7 +14,7 @@ <div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center"> <span class="material-icons text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span> </div> - <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> + <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: fit-content" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> </div> </form> @@ -127,15 +127,6 @@ export default { this.typingTimeout = setTimeout(() => { this.search() }, 250) - this.setInputWidth() - }, - setInputWidth() { - setTimeout(() => { - var value = this.$refs.input.value - var len = value.length * 7 + 24 - this.$refs.input.style.width = len + 'px' - this.recalcMenuPos() - }, 50) }, recalcMenuPos() { if (!this.menu || !this.$refs.inputWrapper) return From 8a0fab2b2080e44a97354832a6446394257d47af Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 5 Mar 2024 14:30:39 -0600 Subject: [PATCH 0415/2145] Fix:Resizing page update chapter ticks and track bar #2707 --- client/components/player/PlayerTrackBar.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/components/player/PlayerTrackBar.vue b/client/components/player/PlayerTrackBar.vue index 2f832785..3afc5d06 100644 --- a/client/components/player/PlayerTrackBar.vue +++ b/client/components/player/PlayerTrackBar.vue @@ -57,7 +57,6 @@ export default { }, watch: { duration: { - immediate: true, handler() { this.setChapterTicks() } @@ -205,10 +204,14 @@ export default { }, windowResize() { this.setTrackWidth() + this.setChapterTicks() + this.updatePlayedTrackWidth() + this.updateBufferTrack() } }, mounted() { this.setTrackWidth() + this.setChapterTicks() window.addEventListener('resize', this.windowResize) }, beforeDestroy() { From 15545654ea1753b7c0841cc08cac9ea79a873abf Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 6 Mar 2024 13:41:54 +0200 Subject: [PATCH 0416/2145] Alternative input width fix in MultiSelect components --- client/components/ui/MultiSelect.vue | 8 ++++++-- client/components/ui/MultiSelectQueryInput.vue | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue index 2009b28d..f1625de0 100644 --- a/client/components/ui/MultiSelect.vue +++ b/client/components/ui/MultiSelect.vue @@ -11,7 +11,7 @@ </div> {{ item }} </div> - <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> + <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> </div> </form> @@ -208,7 +208,10 @@ export default { e.stopPropagation() e.preventDefault() } - if (this.$refs.input) this.$refs.input.focus() + if (this.$refs.input) { + this.$refs.input.style.width = '24px' + this.$refs.input.focus() + } var newSelected = null if (this.selected.includes(itemValue)) { @@ -261,6 +264,7 @@ export default { } else { this.insertNewItem(this.textInput) } + if (this.$refs.input) this.$refs.input.style.width = '24px' }, scroll() { this.recalcMenuPos() diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue index c86d3228..172c83bb 100644 --- a/client/components/ui/MultiSelectQueryInput.vue +++ b/client/components/ui/MultiSelectQueryInput.vue @@ -14,7 +14,7 @@ <div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center"> <span class="material-icons text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span> </div> - <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> + <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> </div> </form> @@ -228,7 +228,10 @@ export default { e.stopPropagation() e.preventDefault() } - if (this.$refs.input) this.$refs.input.focus() + if (this.$refs.input) { + this.$refs.input.style.width = '24px' + this.$refs.input.focus() + } let newSelected = null if (this.getIsSelected(item.id)) { @@ -291,6 +294,7 @@ export default { name: this.textInput }) } + if (this.$refs.input) this.$refs.input.style.width = '24px' }, scroll() { this.recalcMenuPos() From cd60d0219fbbf4aee293d9957cf9a170a83ef97d Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 6 Mar 2024 14:02:15 +0200 Subject: [PATCH 0417/2145] Bring back setInputWidth --- client/components/ui/MultiSelect.vue | 9 +++++++++ client/components/ui/MultiSelectQueryInput.vue | 11 ++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue index 7444223e..f1625de0 100644 --- a/client/components/ui/MultiSelect.vue +++ b/client/components/ui/MultiSelect.vue @@ -110,6 +110,15 @@ export default { this.typingTimeout = setTimeout(() => { this.currentSearch = this.textInput }, 100) + this.setInputWidth() + }, + setInputWidth() { + setTimeout(() => { + var value = this.$refs.input.value + var len = value.length * 7 + 24 + this.$refs.input.style.width = len + 'px' + this.recalcMenuPos() + }, 50) }, recalcMenuPos() { if (!this.menu || !this.$refs.inputWrapper) return diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue index d5fb04b3..65cc8a63 100644 --- a/client/components/ui/MultiSelectQueryInput.vue +++ b/client/components/ui/MultiSelectQueryInput.vue @@ -126,7 +126,16 @@ export default { clearTimeout(this.typingTimeout) this.typingTimeout = setTimeout(() => { this.search() - }, 250) + }, 250) + this.setInputWidth() + }, + setInputWidth() { + setTimeout(() => { + var value = this.$refs.input.value + var len = value.length * 7 + 24 + this.$refs.input.style.width = len + 'px' + this.recalcMenuPos() + }, 50) }, recalcMenuPos() { if (!this.menu || !this.$refs.inputWrapper) return From 4dd140585d95bc30f14448072f7c0766d07b2388 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 6 Mar 2024 15:29:10 -0600 Subject: [PATCH 0418/2145] Add:Abridged checkbox to batch edit overwrite map details #2695 --- client/pages/batch/index.vue | 58 +++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/client/pages/batch/index.vue b/client/pages/batch/index.vue index 15675fb8..e1687f0f 100644 --- a/client/pages/batch/index.vue +++ b/client/pages/batch/index.vue @@ -20,44 +20,44 @@ <div class="overflow-hidden"> <transition name="slide"> <div v-if="openMapOptions" class="flex flex-wrap"> - <div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2"> + <div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2"> <ui-checkbox v-model="selectedBatchUsage.subtitle" /> - <ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-4 ml-4" /> + <ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-5 ml-4" /> </div> - <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2"> + <div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2"> <ui-checkbox v-model="selectedBatchUsage.authors" /> <!-- Authors filter only contains authors in this library, uses filter data --> - <ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" filter-key="authors" class="mb-4 ml-4" /> + <ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" filter-key="authors" class="mb-5 ml-4" /> </div> - <div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2"> + <div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2"> <ui-checkbox v-model="selectedBatchUsage.publishedYear" /> - <ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-4 ml-4" /> + <ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-5 ml-4" /> </div> - <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2"> + <div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2"> <ui-checkbox v-model="selectedBatchUsage.series" /> - <ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="existingSeriesNames" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" /> + <ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="existingSeriesNames" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-5 ml-4" /> </div> - <div class="flex items-center px-4 w-1/2"> + <div class="flex items-center px-4 h-18 w-1/2"> <ui-checkbox v-model="selectedBatchUsage.genres" /> - <ui-multi-select ref="genresSelect" v-model="batchDetails.genres" :disabled="!selectedBatchUsage.genres" :label="$strings.LabelGenres" :items="genreItems" @newItem="newGenreItem" @removedItem="removedGenreItem" class="mb-4 ml-4" /> + <ui-multi-select ref="genresSelect" v-model="batchDetails.genres" :disabled="!selectedBatchUsage.genres" :label="$strings.LabelGenres" :items="genreItems" @newItem="newGenreItem" @removedItem="removedGenreItem" class="mb-5 ml-4" /> </div> - <div class="flex items-center px-4 w-1/2"> + <div class="flex items-center px-4 h-18 w-1/2"> <ui-checkbox v-model="selectedBatchUsage.tags" /> - <ui-multi-select ref="tagsSelect" v-model="batchDetails.tags" :label="$strings.LabelTags" :disabled="!selectedBatchUsage.tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" class="mb-4 ml-4" /> + <ui-multi-select ref="tagsSelect" v-model="batchDetails.tags" :label="$strings.LabelTags" :disabled="!selectedBatchUsage.tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" class="mb-5 ml-4" /> </div> - <div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2"> + <div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2"> <ui-checkbox v-model="selectedBatchUsage.narrators" /> - <ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" /> + <ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-5 ml-4" /> </div> - <div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2"> + <div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2"> <ui-checkbox v-model="selectedBatchUsage.publisher" /> - <ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-4 ml-4" /> + <ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-5 ml-4" /> </div> - <div v-if="!isMapAppend" class="flex items-center px-4 w-1/2"> + <div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2"> <ui-checkbox v-model="selectedBatchUsage.language" /> - <ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-4 ml-4" /> + <ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-5 ml-4" /> </div> - <div v-if="!isMapAppend" class="flex items-center px-4 w-1/2"> + <div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2"> <ui-checkbox v-model="selectedBatchUsage.explicit" /> <div class="ml-4"> <ui-checkbox @@ -71,6 +71,20 @@ /> </div> </div> + <div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2"> + <ui-checkbox v-model="selectedBatchUsage.abridged" /> + <div class="ml-4"> + <ui-checkbox + v-model="batchDetails.abridged" + :label="$strings.LabelAbridged" + :disabled="!selectedBatchUsage.abridged" + :checkbox-bg="!selectedBatchUsage.abridged ? 'bg' : 'primary'" + :check-color="!selectedBatchUsage.abridged ? 'gray-600' : 'green-500'" + border-color="gray-600" + :label-class="!selectedBatchUsage.abridged ? 'pl-2 text-base text-gray-400 font-semibold' : 'pl-2 text-base font-semibold'" + /> + </div> + </div> <div class="w-full flex items-center justify-end p-4"> <ui-btn color="success" :disabled="!hasSelectedBatchUsage" :padding-x="8" small class="text-base" :loading="isProcessing" @click="mapBatchDetails">{{ $strings.ButtonApply }}</ui-btn> @@ -139,7 +153,8 @@ export default { narrators: [], publisher: null, language: null, - explicit: false + explicit: false, + abridged: false }, selectedBatchUsage: { subtitle: false, @@ -151,7 +166,8 @@ export default { narrators: false, publisher: false, language: false, - explicit: false + explicit: false, + abridged: false }, appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'], openMapOptions: false From 305689d5130eb8c481005ef5e9c08e7612af14de Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 7 Mar 2024 12:26:04 -0600 Subject: [PATCH 0419/2145] Update authors sort --- .../pages/library/_library/authors/index.vue | 41 ++++++++----------- client/store/user.js | 2 +- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/client/pages/library/_library/authors/index.vue b/client/pages/library/_library/authors/index.vue index a81865a5..3906f671 100644 --- a/client/pages/library/_library/authors/index.vue +++ b/client/pages/library/_library/authors/index.vue @@ -3,7 +3,7 @@ <app-book-shelf-toolbar page="authors" is-home :authors="authors" /> <div id="bookshelf" class="w-full h-full p-8 overflow-y-auto"> <div class="flex flex-wrap justify-center"> - <template v-for="author in authors"> + <template v-for="author in authorsSorted"> <cards-author-card :key="author.id" :author="author" :width="160" :height="200" class="p-3" @edit="editAuthor" /> </template> </div> @@ -44,16 +44,29 @@ export default { }, selectedAuthor() { return this.$store.state.globals.selectedAuthor + }, + authorSortBy() { + return this.$store.getters['user/getUserSetting']('authorSortBy') || 'name' + }, + authorSortDesc() { + return !!this.$store.getters['user/getUserSetting']('authorSortDesc') + }, + authorsSorted() { + const sortProp = this.authorSortBy + const bDesc = this.authorSortDesc ? -1 : 1 + return this.authors.sort((a, b) => { + if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') { + return a[sortProp] > b[sortProp] ? bDesc : -bDesc + } + return a[sortProp].localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc + }) } }, methods: { async init() { - this.settings = { ...this.$store.state.user.settings } this.authors = await this.$axios .$get(`/api/libraries/${this.currentLibraryId}/authors`) - .then((response) => { - return this.sortAuthors(response.authors) - }) + .then((response) => response.authors) .catch((error) => { console.error('Failed to load authors', error) return [] @@ -81,33 +94,15 @@ export default { }, editAuthor(author) { this.$store.commit('globals/showEditAuthorModal', author) - }, - sortAuthors(authors) { - const sortProp = this.settings.authorSortBy - const bDesc = this.settings.authorSortDesc === true ? -1 : 1 - return authors.sort((a, b) => { - if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') { - return a[sortProp] > b[sortProp] ? 1 * bDesc : -1 * bDesc - } - return a[sortProp].localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc - }) - }, - settingsUpdated(settings) { - for (const key in settings) { - this.settings[key] = settings[key] - } - this.sortAuthors(this.authors) } }, mounted() { this.init() - this.$eventBus.$on('user-settings', this.settingsUpdated) this.$root.socket.on('author_added', this.authorAdded) this.$root.socket.on('author_updated', this.authorUpdated) this.$root.socket.on('author_removed', this.authorRemoved) }, beforeDestroy() { - this.$eventBus.$off('user-settings', this.settingsUpdated) this.$root.socket.off('author_added', this.authorAdded) this.$root.socket.off('author_updated', this.authorUpdated) this.$root.socket.off('author_removed', this.authorRemoved) diff --git a/client/store/user.js b/client/store/user.js index 40a8813f..a69ee058 100644 --- a/client/store/user.js +++ b/client/store/user.js @@ -13,7 +13,7 @@ export const state = () => ({ seriesSortDesc: false, seriesFilterBy: 'all', authorSortBy: 'name', - authorSortDesc: false, + authorSortDesc: false } }) From a5772f6b66c38c8d6589374d042f8f8a629ac561 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Fri, 8 Mar 2024 08:51:05 +0200 Subject: [PATCH 0420/2145] Add keyboard navigation to multi-select components --- client/components/ui/MultiSelect.vue | 84 ++++++++++++++++--- .../components/ui/MultiSelectQueryInput.vue | 55 +++++++++++- 2 files changed, 126 insertions(+), 13 deletions(-) diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue index 2009b28d..9185ef83 100644 --- a/client/components/ui/MultiSelect.vue +++ b/client/components/ui/MultiSelect.vue @@ -17,7 +17,7 @@ <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 itemsToShow"> - <li :key="item" 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> + <li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-sky-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 }}</span> </div> @@ -62,7 +62,9 @@ export default { currentSearch: null, typingTimeout: null, isFocused: false, - menu: null + menu: null, + filteredItems: null, + selectedMenuItemIndex: null } }, watch: { @@ -91,24 +93,66 @@ export default { return classes.join(' ') }, itemsToShow() { - if (!this.currentSearch || !this.textInput) { + if (!this.currentSearch || !this.textInput || !this.filteredItems) { return this.items } - return this.items.filter((i) => { - var iValue = String(i).toLowerCase() - return iValue.includes(this.currentSearch.toLowerCase()) - }) + return this.filteredItems } }, methods: { editItem(item) { this.$emit('edit', item) }, - keydownInput() { + search() { + if (!this.textInput) { + this.filteredItems = null + return + } + this.currentSearch = this.textInput + + const results = this.items.filter((i) => { + var iValue = String(i).toLowerCase() + return iValue.includes(this.currentSearch.toLowerCase()) + }) + + this.filteredItems = results || [] + }, + keydownInput(event) { + let items = this.itemsToShow + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + event.preventDefault() + if (!items.length) return + if (event.key === 'ArrowDown') { + if (this.selectedMenuItemIndex === null) { + this.selectedMenuItemIndex = 0 + } else { + this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1) + } + console.log('ArrowDown. this.selectedMenuItemIndex=', this.selectedMenuItemIndex) + } else if (event.key === 'ArrowUp') { + if (this.selectedMenuItemIndex === null) { + this.selectedMenuItemIndex = items.length - 1 + } else { + this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0) + } + console.log('ArrowUp. this.selectedMenuItemIndex=', this.selectedMenuItemIndex) + } + this.recalcScroll() + return + } else if (event.key === 'Enter') { + if (this.selectedMenuItemIndex !== null) { + this.clickedOption(event, items[this.selectedMenuItemIndex]) + } else { + console.log('Enter. this.textInput=', this.textInput) + this.submitForm() + } + return + } + this.selectedMenuItemIndex = null clearTimeout(this.typingTimeout) this.typingTimeout = setTimeout(() => { - this.currentSearch = this.textInput + this.search() }, 100) this.setInputWidth() }, @@ -120,6 +164,24 @@ export default { this.recalcMenuPos() }, 50) }, + recalcScroll() { + if (!this.menu) return + var menuItems = this.menu.querySelectorAll('li') + if (!menuItems.length) return + var selectedItem = menuItems[this.selectedMenuItemIndex] + if (!selectedItem) return + var menuHeight = this.menu.offsetHeight + var itemHeight = selectedItem.offsetHeight + var itemTop = selectedItem.offsetTop + var itemBottom = itemTop + itemHeight + if (itemBottom > this.menu.scrollTop + menuHeight) { + let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom) + this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom + } else if (itemTop < this.menu.scrollTop) { + let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop) + this.menu.scrollTop = itemTop - menuPaddingTop + } + }, recalcMenuPos() { if (!this.menu || !this.$refs.inputWrapper) return var boundingBox = this.$refs.inputWrapper.getBoundingClientRect() @@ -219,6 +281,7 @@ export default { } this.textInput = null this.currentSearch = null + this.selectedMenuItemIndex = null this.$emit('input', newSelected) this.$nextTick(() => { this.recalcMenuPos() @@ -245,6 +308,7 @@ export default { this.$emit('newItem', item) this.textInput = null this.currentSearch = null + this.selectedMenuItemIndex = null this.$nextTick(() => { this.blur() }) @@ -260,7 +324,7 @@ export default { this.clickedOption(null, matchesItem) } else { this.insertNewItem(this.textInput) - } + } }, scroll() { this.recalcMenuPos() diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue index c86d3228..0cbc3031 100644 --- a/client/components/ui/MultiSelectQueryInput.vue +++ b/client/components/ui/MultiSelectQueryInput.vue @@ -20,7 +20,7 @@ <ul ref="menu" v-show="showMenu" class="absolute z-60 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 itemsToShow"> - <li :key="item.id" 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> + <li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-sky-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.name }}</span> </div> @@ -63,7 +63,8 @@ export default { typingTimeout: null, isFocused: false, menu: null, - items: [] + items: [], + selectedMenuItemIndex: null } }, watch: { @@ -122,7 +123,35 @@ export default { this.items = results || [] }, - keydownInput() { + keydownInput(event) { + let items = this.itemsToShow + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + event.preventDefault() + if (!items.length) return + if (event.key === 'ArrowDown') { + if (this.selectedMenuItemIndex === null) { + this.selectedMenuItemIndex = 0 + } else { + this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1) + } + } else if (event.key === 'ArrowUp') { + if (this.selectedMenuItemIndex === null) { + this.selectedMenuItemIndex = items.length - 1 + } else { + this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0) + } + } + this.recalcScroll() + return + } else if (event.key === 'Enter') { + if (this.selectedMenuItemIndex !== null) { + this.clickedOption(event, items[this.selectedMenuItemIndex]) + } else { + this.submitForm() + } + return + } + this.selectedMenuItemIndex = null clearTimeout(this.typingTimeout) this.typingTimeout = setTimeout(() => { this.search() @@ -137,6 +166,24 @@ export default { this.recalcMenuPos() }, 50) }, + recalcScroll() { + if (!this.menu) return + var menuItems = this.menu.querySelectorAll('li') + if (!menuItems.length) return + var selectedItem = menuItems[this.selectedMenuItemIndex] + if (!selectedItem) return + var menuHeight = this.menu.offsetHeight + var itemHeight = selectedItem.offsetHeight + var itemTop = selectedItem.offsetTop + var itemBottom = itemTop + itemHeight + if (itemBottom > this.menu.scrollTop + menuHeight) { + let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom) + this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom + } else if (itemTop < this.menu.scrollTop) { + let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop) + this.menu.scrollTop = itemTop - menuPaddingTop + } + }, recalcMenuPos() { if (!this.menu || !this.$refs.inputWrapper) return var boundingBox = this.$refs.inputWrapper.getBoundingClientRect() @@ -244,6 +291,7 @@ export default { } this.textInput = null this.currentSearch = null + this.selectedMenuItemIndex = null this.$emit('input', newSelected) this.$nextTick(() => { @@ -271,6 +319,7 @@ export default { this.$emit('newItem', item) this.textInput = null this.currentSearch = null + this.selectedMenuItemIndex = null this.$nextTick(() => { this.blur() }) From 0e8148001e0865996b3b8cd7eb56d15dbcd5e14d Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sat, 9 Mar 2024 11:59:50 +0200 Subject: [PATCH 0421/2145] Fix direct access to Database.libraryFilterData --- server/Database.js | 16 +++++++++++++++- server/scanner/BookScanner.js | 8 ++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/server/Database.js b/server/Database.js index 3d4219e5..2d38cd35 100644 --- a/server/Database.js +++ b/server/Database.js @@ -430,7 +430,7 @@ class Database { await oldLibraryItem.saveMetadata() const updated = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem) // Clear library filter data cache - if (updated) { + if (updated) { delete this.libraryFilterData[oldLibraryItem.libraryId] } return updated @@ -689,6 +689,20 @@ class Database { return this.libraryFilterData[libraryId].series.some(se => se.id === seriesId) } + async getAuthorByName(libraryId, authorName) { + if (!this.libraryFilterData[libraryId]) { + return await this.authorModel.getOldByNameAndLibrary(authorName, libraryId) + } + return this.libraryFilterData[libraryId].authors.find(au => au.name === authorName) + } + + async getSeriesByName(libraryId, seriesName) { + if (!this.libraryFilterData[libraryId]) { + return await this.seriesModel.getOldByNameAndLibrary(seriesName, libraryId) + } + return this.libraryFilterData[libraryId].series.find(se => se.name === seriesName) + } + /** * Reset numIssues for library * @param {string} libraryId diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 85bf8146..fbe3ccfb 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -186,7 +186,7 @@ class BookScanner { // Check for authors added for (const authorName of bookMetadata.authors) { if (!media.authors.some(au => au.name === authorName)) { - const existingAuthor = Database.libraryFilterData[libraryItemData.libraryId].authors.find(au => au.name === authorName) + const existingAuthor = await Database.getAuthorByName(libraryItemData.libraryId, authorName) if (existingAuthor) { await Database.bookAuthorModel.create({ bookId: media.id, @@ -221,7 +221,7 @@ class BookScanner { for (const seriesObj of bookMetadata.series) { const existingBookSeries = media.series.find(se => se.name === seriesObj.name) if (!existingBookSeries) { - const existingSeries = Database.libraryFilterData[libraryItemData.libraryId].series.find(se => se.name === seriesObj.name) + const existingSeries = await Database.getSeriesByName(libraryItemData.libraryId, seriesObj.name) if (existingSeries) { await Database.bookSeriesModel.create({ bookId: media.id, @@ -443,7 +443,7 @@ class BookScanner { } if (bookMetadata.authors.length) { for (const authorName of bookMetadata.authors) { - const matchingAuthor = Database.libraryFilterData[libraryItemData.libraryId].authors.find(au => au.name === authorName) + const matchingAuthor = await Database.getAuthorByName(libraryItemData.libraryId, authorName) if (matchingAuthor) { bookObject.bookAuthors.push({ authorId: matchingAuthor.id @@ -463,7 +463,7 @@ class BookScanner { if (bookMetadata.series.length) { for (const seriesObj of bookMetadata.series) { if (!seriesObj.name) continue - const matchingSeries = Database.libraryFilterData[libraryItemData.libraryId].series.find(se => se.name === seriesObj.name) + const matchingSeries = await Database.getSeriesByName(libraryItemData.libraryId, seriesObj.name) if (matchingSeries) { bookObject.bookSeries.push({ seriesId: matchingSeries.id, From ad45dadc15ebd881a2e75c127ea9650fe8f30cc2 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sat, 9 Mar 2024 12:07:08 +0200 Subject: [PATCH 0422/2145] Remove redundant space --- server/Database.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Database.js b/server/Database.js index 2d38cd35..78d6bc7d 100644 --- a/server/Database.js +++ b/server/Database.js @@ -430,7 +430,7 @@ class Database { await oldLibraryItem.saveMetadata() const updated = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem) // Clear library filter data cache - if (updated) { + if (updated) { delete this.libraryFilterData[oldLibraryItem.libraryId] } return updated From daa9fccc14cfd90010327004db8950f52165f4f2 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Sat, 9 Mar 2024 23:00:01 +0000 Subject: [PATCH 0423/2145] Add: Vietnamese translations --- client/plugins/i18n.js | 1 + client/strings/vi-vn.json | 751 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 752 insertions(+) create mode 100644 client/strings/vi-vn.json diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 021682cf..4a88d6f6 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -22,6 +22,7 @@ const languageCodeMap = { 'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' }, 'ru': { label: 'Русский', dateFnsLocale: 'ru' }, 'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, + 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'viVN' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, } Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { diff --git a/client/strings/vi-vn.json b/client/strings/vi-vn.json new file mode 100644 index 00000000..c150cc7e --- /dev/null +++ b/client/strings/vi-vn.json @@ -0,0 +1,751 @@ +{ + "ButtonAdd": "Thêm", + "ButtonAddChapters": "Thêm Chương", + "ButtonAddDevice": "Thêm Thiết Bị", + "ButtonAddLibrary": "Thêm Thư Viện", + "ButtonAddPodcasts": "Thêm Podcasts", + "ButtonAddUser": "Thêm Người Dùng", + "ButtonAddYourFirstLibrary": "Thêm thư viện đầu tiên của bạn", + "ButtonApply": "Áp Dụng", + "ButtonApplyChapters": "Áp Dụng Chương", + "ButtonAuthors": "Tác Giả", + "ButtonBrowseForFolder": "Duyệt Thư Mục", + "ButtonCancel": "Hủy", + "ButtonCancelEncode": "Hủy Mã Hóa", + "ButtonChangeRootPassword": "Thay Đổi Mật Khẩu Root", + "ButtonCheckAndDownloadNewEpisodes": "Kiểm Tra và Tải Xuống Các Tập Phim Mới", + "ButtonChooseAFolder": "Chọn một thư mục", + "ButtonChooseFiles": "Chọn tập tin", + "ButtonClearFilter": "Xóa Bộ Lọc", + "ButtonCloseFeed": "Đóng Feed", + "ButtonCollections": "Bộ Sưu Tập", + "ButtonConfigureScanner": "Cấu Hình Bộ Quét", + "ButtonCreate": "Tạo", + "ButtonCreateBackup": "Tạo Bản Sao Lưu", + "ButtonDelete": "Xóa", + "ButtonDownloadQueue": "Hàng Chờ", + "ButtonEdit": "Chỉnh Sửa", + "ButtonEditChapters": "Chỉnh Sửa Chương", + "ButtonEditPodcast": "Chỉnh Sửa Podcast", + "ButtonForceReScan": "Force Re-Scan", + "ButtonFullPath": "Đường Dẫn Đầy Đủ", + "ButtonHide": "Ẩn", + "ButtonHome": "Trang Chủ", + "ButtonIssues": "Vấn Đề", + "ButtonJumpBackward": "Bước Lùi", + "ButtonJumpForward": "Bước Tiến", + "ButtonLatest": "Mới Nhất", + "ButtonLibrary": "Thư Viện", + "ButtonLogout": "Đăng Xuất", + "ButtonLookup": "Tra Cứu", + "ButtonManageTracks": "Quản Lý Tracks", + "ButtonMapChapterTitles": "Ánh Xạ Tiêu Đề Chương", + "ButtonMatchAllAuthors": "Khớp Tất Cả Tác Giả", + "ButtonMatchBooks": "Khớp Sách", + "ButtonNevermind": "Không Sao", + "ButtonNext": "Tiếp Theo", + "ButtonNextChapter": "Chương Tiếp Theo", + "ButtonOk": "Ok", + "ButtonOpenFeed": "Mở Feed", + "ButtonOpenManager": "Mở Quản Lý", + "ButtonPause": "Tạm Dừng", + "ButtonPlay": "Phát", + "ButtonPlaying": "Đang Phát", + "ButtonPlaylists": "Danh Sách Phát", + "ButtonPrevious": "Trước", + "ButtonPreviousChapter": "Chương Trước", + "ButtonPurgeAllCache": "Xóa Sạch Tất Cả Bộ Nhớ Cache", + "ButtonPurgeItemsCache": "Xóa Sạch Bộ Nhớ Cache Các Mục", + "ButtonPurgeMediaProgress": "Xóa Sạch Tiến Trình Phương Tiện", + "ButtonQueueAddItem": "Thêm vào hàng đợi", + "ButtonQueueRemoveItem": "Xóa khỏi hàng đợi", + "ButtonQuickMatch": "Khớp Nhanh", + "ButtonRead": "Đọc", + "ButtonRefresh": "Làm Mới", + "ButtonRemove": "Xóa", + "ButtonRemoveAll": "Xóa Tất Cả", + "ButtonRemoveAllLibraryItems": "Xóa Tất Cả Các Mục Thư Viện", + "ButtonRemoveFromContinueListening": "Xóa khỏi Tiếp Tục Nghe", + "ButtonRemoveFromContinueReading": "Xóa khỏi Tiếp Tục Đọc", + "ButtonRemoveSeriesFromContinueSeries": "Xóa Series khỏi Tiếp Tục Series", + "ButtonReScan": "Quét Lại", + "ButtonReset": "Đặt Lại", + "ButtonResetToDefault": "Đặt Lại về Mặc Định", + "ButtonRestore": "Khôi Phục", + "ButtonSave": "Lưu", + "ButtonSaveAndClose": "Lưu & Đóng", + "ButtonSaveTracklist": "Lưu Danh Sách Track", + "ButtonScan": "Quét", + "ButtonScanLibrary": "Quét Thư Viện", + "ButtonSearch": "Tìm Kiếm", + "ButtonSelectFolderPath": "Chọn Đường Dẫn Thư Mục", + "ButtonSeries": "Series", + "ButtonSetChaptersFromTracks": "Đặt chương từ các track", + "ButtonShare": "Chia Sẻ", + "ButtonShiftTimes": "Dời Thời Gian", + "ButtonShow": "Hiện", + "ButtonStartM4BEncode": "Bắt đầu Mã Hóa M4B", + "ButtonStartMetadataEmbed": "Bắt đầu Nhúng Dữ Liệu", + "ButtonSubmit": "Gửi", + "ButtonTest": "Kiểm Tra", + "ButtonUpload": "Tải Lên", + "ButtonUploadBackup": "Tải Lên Bản Sao Lưu", + "ButtonUploadCover": "Tải Lên Bìa", + "ButtonUploadOPMLFile": "Tải Lên Tệp OPML", + "ButtonUserDelete": "Xóa người dùng {0}", + "ButtonUserEdit": "Chỉnh Sửa người dùng {0}", + "ButtonViewAll": "Xem Tất Cả", + "ButtonYes": "Có", + "ErrorUploadFetchMetadataAPI": "Lỗi khi lấy dữ liệu metadata", + "ErrorUploadFetchMetadataNoResults": "Không thể lấy dữ liệu metadata - hãy thử cập nhật tiêu đề và/hoặc tác giả", + "ErrorUploadLacksTitle": "Phải có một tiêu đề", + "HeaderAccount": "Tài Khoản", + "HeaderAdvanced": "Nâng Cao", + "HeaderAppriseNotificationSettings": "Cài Đặt Thông Báo Apprise", + "HeaderAudiobookTools": "Công Cụ Quản Lý Tệp Truyện Nói", + "HeaderAudioTracks": "Các Track Âm Thanh", + "HeaderAuthentication": "Xác Thực", + "HeaderBackups": "Bản Sao Lưu", + "HeaderChangePassword": "Thay Đổi Mật Khẩu", + "HeaderChapters": "Chương", + "HeaderChooseAFolder": "Chọn Một Thư Mục", + "HeaderCollection": "Bộ Sưu Tập", + "HeaderCollectionItems": "Các Mục Bộ Sưu Tập", + "HeaderCover": "Bìa", + "HeaderCurrentDownloads": "Tải Xuống Hiện Tại", + "HeaderCustomMetadataProviders": "Các Nhà Cung Cấp Metadata Tùy Chỉnh", + "HeaderDetails": "Chi Tiết", + "HeaderDownloadQueue": "Hàng Đợi Tải Xuống", + "HeaderEbookFiles": "Tệp Ebook", + "HeaderEmail": "Email", + "HeaderEmailSettings": "Cài Đặt Email", + "HeaderEpisodes": "Tập Phim", + "HeaderEreaderDevices": "Thiết Bị Đọc Sách", + "HeaderEreaderSettings": "Cài Đặt Thiết Bị Đọc Sách", + "HeaderFiles": "Tệp", + "HeaderFindChapters": "Tìm Kiếm Chương", + "HeaderIgnoredFiles": "Tệp Bị Bỏ Qua", + "HeaderItemFiles": "Tệp Mục", + "HeaderItemMetadataUtils": "Công Cụ Metadata Mục", + "HeaderLastListeningSession": "Phiên Nghe Gần Nhất", + "HeaderLatestEpisodes": "Tập Mới Nhất", + "HeaderLibraries": "Thư Viện", + "HeaderLibraryFiles": "Tệp Thư Viện", + "HeaderLibraryStats": "Thống Kê Thư Viện", + "HeaderListeningSessions": "Phiên Nghe", + "HeaderListeningStats": "Thống Kê Nghe", + "HeaderLogin": "Đăng Nhập", + "HeaderLogs": "Nhật Ký", + "HeaderManageGenres": "Quản Lý Thể Loại", + "HeaderManageTags": "Quản Lý Thẻ", + "HeaderMapDetails": "Bản Đồ Chi Tiết", + "HeaderMatch": "Kết Hợp", + "HeaderMetadataOrderOfPrecedence": "Thứ Tự Ưu Tiên Metadata", + "HeaderMetadataToEmbed": "Metadata để nhúng", + "HeaderNewAccount": "Tài Khoản Mới", + "HeaderNewLibrary": "Thư Viện Mới", + "HeaderNotifications": "Thông Báo", + "HeaderOpenIDConnectAuthentication": "Xác Thực Mở ID Connect", + "HeaderOpenRSSFeed": "Mở RSS Feed", + "HeaderOtherFiles": "Các Tệp Khác", + "HeaderPasswordAuthentication": "Xác Thực Mật Khẩu", + "HeaderPermissions": "Quyền Hạn", + "HeaderPlayerQueue": "Hàng Đợi Người Chơi", + "HeaderPlaylist": "Danh Sách Phát", + "HeaderPlaylistItems": "Các Mục Danh Sách Phát", + "HeaderPodcastsToAdd": "Podcasts để Thêm", + "HeaderPreviewCover": "Xem Trước Bìa", + "HeaderRemoveEpisode": "Xóa Tập", + "HeaderRemoveEpisodes": "Xóa {0} Tập", + "HeaderRSSFeedGeneral": "Chi Tiết RSS", + "HeaderRSSFeedIsOpen": "RSS Feed Đã Mở", + "HeaderRSSFeeds": "RSS Feeds", + "HeaderSavedMediaProgress": "Tiến Trình Phương Tiện Đã Lưu", + "HeaderSchedule": "Lịch Trình", + "HeaderScheduleLibraryScans": "Lên Lịch Quét Tự Động Thư Viện", + "HeaderSession": "Phiên", + "HeaderSetBackupSchedule": "Đặt Lịch Sao Lưu", + "HeaderSettings": "Cài Đặt", + "HeaderSettingsDisplay": "Hiển Thị", + "HeaderSettingsExperimental": "Tính Năng Thử Nghiệm", + "HeaderSettingsGeneral": "Chung", + "HeaderSettingsScanner": "Máy Quét", + "HeaderSleepTimer": "Hẹn Giờ Tắt", + "HeaderStatsLargestItems": "Các Mục Lớn Nhất", + "HeaderStatsLongestItems": "Các Mục Dài Nhất (giờ)", + "HeaderStatsMinutesListeningChart": "Thống Kê Thời Gian Nghe (7 ngày gần nhất)", + "HeaderStatsRecentSessions": "Các Phiên Gần Đây", + "HeaderStatsTop10Authors": "10 Tác Giả Hàng Đầu", + "HeaderStatsTop5Genres": "5 Thể Loại Hàng Đầu", + "HeaderTableOfContents": "Mục Lục", + "HeaderTools": "Công Cụ", + "HeaderUpdateAccount": "Cập Nhật Tài Khoản", + "HeaderUpdateAuthor": "Cập Nhật Tác Giả", + "HeaderUpdateDetails": "Cập Nhật Chi Tiết", + "HeaderUpdateLibrary": "Cập Nhật Thư Viện", + "HeaderUsers": "Người Dùng", + "HeaderYearReview": "Năm {0} trong Xem Xét", + "HeaderYourStats": "Thống Kê Của Bạn", + "LabelAbridged": "Rút Gọn", + "LabelAccountType": "Loại Tài Khoản", + "LabelAccountTypeAdmin": "Quản Trị Viên", + "LabelAccountTypeGuest": "Khách", + "LabelAccountTypeUser": "Người Dùng", + "LabelActivity": "Hoạt Động", + "LabelAdded": "Đã Thêm", + "LabelAddedAt": "Đã Thêm Lúc", + "LabelAddToCollection": "Thêm vào Bộ Sưu Tập", + "LabelAddToCollectionBatch": "Thêm {0} Sách vào Bộ Sưu Tập", + "LabelAddToPlaylist": "Thêm vào Danh Sách Phát", + "LabelBooks": "Sách", + "LabelButtonText": "Nút Văn Bản", + "LabelChangePassword": "Đổi Mật Khẩu", + "LabelChannels": "Kênh", + "LabelChapters": "Chương", + "LabelChaptersFound": "chương được tìm thấy", + "LabelChapterTitle": "Tiêu đề Chương", + "LabelClickForMoreInfo": "Nhấn để biết thêm thông tin", + "LabelClosePlayer": "Đóng trình phát", + "LabelCodec": "Mã hóa", + "LabelCollapseSeries": "Thu gọn Series", + "LabelCollection": "Bộ Sưu Tập", + "LabelCollections": "Các Bộ Sưu Tập", + "LabelComplete": "Hoàn Thành", + "LabelConfirmPassword": "Xác Nhận Mật Khẩu", + "LabelContinueListening": "Tiếp Tục Nghe", + "LabelContinueReading": "Tiếp Tục Đọc", + "LabelContinueSeries": "Tiếp Tục Series", + "LabelCover": "Bìa", + "LabelCoverImageURL": "URL Ảnh Bìa", + "LabelCreatedAt": "Được Tạo Lúc", + "LabelCronExpression": "Biểu Thức Cron", + "LabelCurrent": "Hiện tại", + "LabelCurrently": "Hiện tại:", + "LabelCustomCronExpression": "Biểu Thức Cron Tùy Chỉnh:", + "LabelDatetime": "Ngày giờ", + "LabelDeleteFromFileSystemCheckbox": "Xóa khỏi hệ thống tệp (bỏ chọn để chỉ xóa khỏi cơ sở dữ liệu)", + "LabelDescription": "Mô Tả", + "LabelDeselectAll": "Bỏ Chọn Tất Cả", + "LabelDevice": "Thiết Bị", + "LabelDeviceInfo": "Thông Tin Thiết Bị", + "LabelDeviceIsAvailableTo": "Thiết Bị Đã Sẵn Sàng Cho...", + "LabelDirectory": "Thư Mục", + "LabelDiscFromFilename": "Đĩa từ Tên Tệp", + "LabelDiscFromMetadata": "Đĩa từ Metadata", + "LabelDiscover": "Khám Phá", + "LabelDownload": "Tải Xuống", + "LabelDownloadNEpisodes": "Tải Xuống {0} Tập", + "LabelDuration": "Thời Lượng", + "LabelDurationFound": "Thời lượng được tìm thấy:", + "LabelEbook": "Ebook", + "LabelEbooks": "Các Ebook", + "LabelEdit": "Chỉnh Sửa", + "LabelEmail": "Email", + "LabelEmailSettingsFromAddress": "Địa chỉ Gửi từ", + "LabelEmailSettingsSecure": "Bảo Mật", + "LabelEmailSettingsSecureHelp": "Nếu đúng thì kết nối sẽ sử dụng TLS khi kết nối đến máy chủ. Nếu sai thì TLS sẽ được sử dụng nếu máy chủ hỗ trợ phần mở rộng STARTTLS. Trong hầu hết các trường hợp, hãy đặt giá trị này là đúng nếu bạn kết nối đến cổng 465. Đối với cổng 587 hoặc 25, giữ nó sai. (từ nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Địa Chỉ Kiểm Tra", + "LabelEmbeddedCover": "Bìa Nội", + "LabelEnable": "Bật", + "LabelEnd": "Kết Thúc", + "LabelEpisode": "Tập", + "LabelEpisodeTitle": "Tiêu Đề Tập", + "LabelEpisodeType": "Loại Tập", + "LabelExample": "Ví Dụ", + "LabelExplicit": "Rõ Ràng", + "LabelFeedURL": "URL Feed", + "LabelFetchingMetadata": "Đang Lấy Metadata", + "LabelFile": "Tệp", + "LabelFileBirthtime": "Thời Gian Tạo Tệp", + "LabelFileModified": "Sửa Đổi Tệp", + "LabelFilename": "Tên Tệp", + "LabelFilterByUser": "Lọc theo Người Dùng", + "LabelFindEpisodes": "Tìm Tập", + "LabelFinished": "Hoàn Thành", + "LabelFolder": "Thư Mục", + "LabelFolders": "Các Thư Mục", + "LabelFontBold": "Đậm", + "LabelFontFamily": "Gia đình font", + "LabelFontItalic": "Nghiêng", + "LabelFontScale": "Tỷ lệ font", + "LabelFontStrikethrough": "Gạch ngang", + "LabelFormat": "Định dạng", + "LabelGenre": "Thể loại", + "LabelGenres": "Các thể loại", + "LabelHardDeleteFile": "Xóa tập tin vĩnh viễn", + "LabelHasEbook": "Có ebook", + "LabelHasSupplementaryEbook": "Có ebook bổ sung", + "LabelHighestPriority": "Ưu tiên cao nhất", + "LabelHost": "Máy chủ", + "LabelHour": "Giờ", + "LabelIcon": "Biểu tượng", + "LabelImageURLFromTheWeb": "URL hình ảnh từ web", + "LabelIncludeInTracklist": "Bao gồm trong danh sách phát", + "LabelIncomplete": "Chưa hoàn thành", + "LabelInProgress": "Đang tiến hành", + "LabelInterval": "Khoảng cách", + "LabelIntervalCustomDailyWeekly": "Tuỳ chỉnh hàng ngày/hàng tuần", + "LabelIntervalEvery12Hours": "Mỗi 12 giờ", + "LabelIntervalEvery15Minutes": "Mỗi 15 phút", + "LabelIntervalEvery2Hours": "Mỗi 2 giờ", + "LabelIntervalEvery30Minutes": "Mỗi 30 phút", + "LabelIntervalEvery6Hours": "Mỗi 6 giờ", + "LabelIntervalEveryDay": "Mỗi ngày", + "LabelIntervalEveryHour": "Mỗi giờ", + "LabelInvalidParts": "Phần không hợp lệ", + "LabelInvert": "Nghịch đảo", + "LabelItem": "Mục", + "LabelLanguage": "Ngôn ngữ", + "LabelLanguageDefaultServer": "Ngôn ngữ Máy chủ mặc định", + "LabelLastBookAdded": "Sách mới nhất được thêm", + "LabelLastBookUpdated": "Sách mới nhất được cập nhật", + "LabelLastSeen": "Lần cuối nhìn thấy", + "LabelLastTime": "Lần cuối", + "LabelLastUpdate": "Cập nhật cuối cùng", + "LabelLayout": "Bố cục", + "LabelLayoutSinglePage": "Một trang", + "LabelLayoutSplitPage": "Chia trang", + "LabelLess": "Ít hơn", + "LabelLibrariesAccessibleToUser": "Thư viện có thể truy cập cho người dùng", + "LabelLibrary": "Thư viện", + "LabelLibraryItem": "Mục thư viện", + "LabelLibraryName": "Tên thư viện", + "LabelLimit": "Giới hạn", + "LabelLineSpacing": "Khoảng cách dòng", + "LabelListenAgain": "Nghe lại", + "LabelLogLevelDebug": "Gỡ lỗi", + "LabelLogLevelInfo": "Thông tin", + "LabelLogLevelWarn": "Cảnh báo", + "LabelLookForNewEpisodesAfterDate": "Tìm tập mới sau ngày này", + "LabelLowestPriority": "Ưu tiên thấp nhất", + "LabelMatchExistingUsersBy": "Kết hợp người dùng hiện có theo", + "LabelMatchExistingUsersByDescription": "Sử dụng để kết nối người dùng hiện có. Khi kết nối, người dùng sẽ được kết hợp bằng một ID duy nhất từ nhà cung cấp SSO của bạn", + "LabelMediaPlayer": "Trình phát đa phương tiện", + "LabelMediaType": "Loại phương tiện", + "LabelMetadataOrderOfPrecedenceDescription": "Nguồn siêu dữ liệu ưu tiên cao hơn sẽ ghi đè lên các nguồn siêu dữ liệu ưu tiên thấp hơn", + "LabelMetadataProvider": "Nhà cung cấp siêu dữ liệu", + "LabelMetaTag": "Thẻ Meta", + "LabelMetaTags": "Các thẻ Meta", + "LabelMinute": "Phút", + "LabelMissing": "Thiếu", + "LabelMissingEbook": "Không có ebook", + "LabelMissingParts": "Các phần thiếu", + "LabelMissingSupplementaryEbook": "Không có ebook bổ sung", + "LabelMobileRedirectURIs": "URI chuyển hướng di động được cho phép", + "LabelMobileRedirectURIsDescription": "Đây là danh sách trắng các URI chuyển hướng hợp lệ cho ứng dụng di động. Mặc định là <code>audiobookshelf://oauth</code>, bạn có thể loại bỏ hoặc bổ sung thêm các URI cho tích hợp ứng dụng bên thứ ba. Sử dụng dấu hoa thị (<code>*</code>) như một mục duy nhất cho phép bất kỳ URI nào.", + "LabelMore": "Thêm", + "LabelMoreInfo": "Thông tin thêm", + "LabelName": "Tên", + "LabelNarrator": "Người kể", + "LabelNarrators": "Các người kể", + "LabelNew": "Mới", + "LabelNewestAuthors": "Nhà văn mới nhất", + "LabelNewestEpisodes": "Tập mới nhất", + "LabelNewPassword": "Mật khẩu mới", + "LabelNextBackupDate": "Ngày sao lưu tiếp theo", + "LabelNextScheduledRun": "Chạy tiếp theo theo lịch trình", + "LabelNoEpisodesSelected": "Không có tập nào được chọn", + "LabelNotes": "Ghi chú", + "LabelNotFinished": "Chưa hoàn thành", + "LabelNotificationAppriseURL": "URL(s) thông báo", + "LabelNotificationAvailableVariables": "Biến có sẵn", + "LabelNotificationBodyTemplate": "Mẫu Nội dung", + "LabelNotificationEvent": "Sự kiện Thông báo", + "LabelNotificationsMaxFailedAttempts": "Số lần thất bại tối đa", + "LabelNotificationsMaxFailedAttemptsHelp": "Thông báo sẽ bị vô hiệu hóa sau khi thất bại gửi số lần này", + "LabelNotificationsMaxQueueSize": "Kích thước hàng đợi tối đa cho sự kiện thông báo", + "LabelNotificationsMaxQueueSizeHelp": "Các sự kiện bị giới hạn mỗi giây chỉ gửi 1 lần. Các sự kiện sẽ bị bỏ qua nếu hàng đợi đạt kích thước tối đa. Điều này ngăn chặn spam thông báo.", + "LabelNotificationTitleTemplate": "Mẫu Tiêu đề", + "LabelNotStarted": "Chưa bắt đầu", + "LabelNumberOfBooks": "Số lượng Sách", + "LabelNumberOfEpisodes": "# của Tập", + "LabelOpenRSSFeed": "Mở RSS Feed", + "LabelOverwrite": "Ghi đè", + "LabelPassword": "Mật khẩu", + "LabelPath": "Đường dẫn", + "LabelPermissionsAccessAllLibraries": "Có Thể Truy Cập Tất Cả Thư Viện", + "LabelPermissionsAccessAllTags": "Có Thể Truy Cập Tất Cả Thẻ", + "LabelPermissionsAccessExplicitContent": "Có Thể Truy Cập Nội Dung Rõ Ràng", + "LabelPermissionsDelete": "Có Thể Xóa", + "LabelPermissionsDownload": "Có Thể Tải Xuống", + "LabelPermissionsUpdate": "Có Thể Cập Nhật", + "LabelPermissionsUpload": "Có Thể Tải Lên", + "LabelPersonalYearReview": "Năm của Bạn trong Bài Đánh Giá ({0})", + "LabelPhotoPathURL": "Đường dẫn/URL ảnh", + "LabelPlaylists": "Danh sách phát", + "LabelPlayMethod": "Phương pháp phát", + "LabelPodcast": "Podcast", + "LabelPodcasts": "Các podcast", + "LabelPodcastSearchRegion": "Vùng tìm kiếm podcast", + "LabelPodcastType": "Loại Podcast", + "LabelPort": "Cổng", + "LabelPrefixesToIgnore": "Tiền tố để bỏ qua (không phân biệt chữ hoa/chữ thường)", + "LabelPreventIndexing": "Ngăn chặn feed của bạn được chỉ mục bởi thư mục podcast của iTunes và Google", + "LabelPrimaryEbook": "Ebook chính", + "LabelProgress": "Tiến độ", + "LabelProvider": "Nhà cung cấp", + "LabelPubDate": "Ngày Xuất bản", + "LabelPublisher": "Nhà xuất bản", + "LabelPublishYear": "Năm Xuất bản", + "LabelRead": "Đọc", + "LabelReadAgain": "Đọc lại", + "LabelReadEbookWithoutProgress": "Đọc ebook mà không giữ tiến độ", + "LabelRecentlyAdded": "Gần đây thêm vào", + "LabelRecentSeries": "Loạt phim gần đây", + "LabelRecommended": "Được khuyến nghị", + "LabelRedo": "Làm lại", + "LabelRegion": "Khu vực", + "LabelReleaseDate": "Ngày Phát hành", + "LabelRemoveCover": "Xóa ảnh bìa", + "LabelRowsPerPage": "Số dòng mỗi trang", + "LabelRSSFeedCustomOwnerEmail": "Email chủ sở hữu tùy chỉnh", + "LabelRSSFeedCustomOwnerName": "Tên chủ sở hữu tùy chỉnh", + "LabelRSSFeedOpen": "Mở RSS Feed", + "LabelRSSFeedPreventIndexing": "Ngăn chặn Chỉ mục RSS Feed", + "LabelRSSFeedSlug": "Slug RSS Feed", + "LabelRSSFeedURL": "URL RSS Feed", + "LabelSearchTerm": "Thuật ngữ tìm kiếm", + "LabelSearchTitle": "Tìm kiếm Tiêu đề", + "LabelSearchTitleOrASIN": "Tìm kiếm Tiêu đề hoặc ASIN", + "LabelSeason": "Mùa", + "LabelSelectAllEpisodes": "Chọn tất cả các tập", + "LabelSelectEpisodesShowing": "Chọn {0} tập đang hiển thị", + "LabelSelectUsers": "Chọn người dùng", + "LabelSendEbookToDevice": "Gửi Ebook tới...", + "LabelSequence": "Trình tự", + "LabelSeries": "Loạt", + "LabelSeriesName": "Tên loạt", + "LabelSeriesProgress": "Tiến độ loạt", + "LabelServerYearReview": "Năm của Máy chủ trong Bài Đánh Giá ({0})", + "LabelSetEbookAsPrimary": "Đặt làm chính", + "LabelSetEbookAsSupplementary": "Đặt là bổ sung", + "LabelSettingsAudiobooksOnly": "Chỉ sách nói", + "LabelSettingsAudiobooksOnlyHelp": "Bật cài đặt này sẽ bỏ qua các tập tin ebook trừ khi chúng ở trong một thư mục sách nói, trong trường hợp đó chúng sẽ được đặt làm ebook bổ sung", + "LabelSettingsBookshelfViewHelp": "Thiết kế giả lập với kệ gỗ", + "LabelSettingsChromecastSupport": "Hỗ trợ Chromecast", + "LabelSettingsDateFormat": "Định dạng Ngày", + "LabelSettingsDisableWatcher": "Tắt Watcher", + "LabelSettingsDisableWatcherForLibrary": "Tắt watcher thư mục cho thư viện", + "LabelSettingsDisableWatcherHelp": "Tắt chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ", + "LabelSettingsEnableWatcher": "Bật Watcher", + "LabelSettingsEnableWatcherForLibrary": "Bật watcher thư mục cho thư viện", + "LabelSettingsEnableWatcherHelp": "Bật chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ", + "LabelSettingsExperimentalFeatures": "Tính năng thử nghiệm", + "LabelSettingsExperimentalFeaturesHelp": "Các tính năng đang phát triển có thể cần phản hồi của bạn và sự giúp đỡ trong thử nghiệm. Nhấp để mở thảo luận trên github.", + "LabelSettingsFindCovers": "Tìm ảnh bìa", + "LabelSettingsFindCoversHelp": "Nếu sách nói của bạn không có ảnh bìa nhúng hoặc ảnh bìa trong thư mục, trình quét sẽ cố gắng tìm ảnh bìa.<br>Lưu ý: Điều này sẽ kéo dài thời gian quét", + "LabelSettingsHideSingleBookSeries": "Ẩn loạt sách đơn lẻ", + "LabelSettingsHideSingleBookSeriesHelp": "Các loạt sách chỉ có một cuốn sách sẽ được ẩn khỏi trang loạt sách và kệ trang chủ.", + "LabelSettingsHomePageBookshelfView": "Trang chủ sử dụng chế độ xem kệ sách", + "LabelSettingsLibraryBookshelfView": "Thư viện sử dụng chế độ xem kệ sách", + "LabelSettingsParseSubtitles": "Phân tích phụ đề", + "LabelSettingsParseSubtitlesHelp": "Trích xuất phụ đề từ tên thư mục sách nói.<br>Phụ đề phải được tách bằng \" - \"<br>i.e. \"Book Title - A Subtitle Here\" có phụ đề \"A Subtitle Here\"", + "LabelSettingsPreferMatchedMetadata": "Ưu tiên siêu dữ liệu phù hợp", + "LabelSettingsPreferMatchedMetadataHelp": "Dữ liệu phù hợp sẽ ghi đè lên chi tiết mục khi sử dụng Kết hợp Nhanh. Theo mặc định, Kết hợp Nhanh chỉ điền vào các chi tiết bị thiếu.", + "LabelSettingsSkipMatchingBooksWithASIN": "Bỏ qua sách khớp có ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Bỏ qua sách khớp có ISBN", + "LabelSettingsSortingIgnorePrefixes": "Bỏ qua tiền tố khi sắp xếp", + "LabelSettingsSortingIgnorePrefixesHelp": "ví dụ. với tiền tố \"the\" tiêu đề sách \"The Book Title\" sẽ được sắp xếp như \"Book Title, The\"", + "LabelSettingsSquareBookCovers": "Sử dụng ảnh bìa vuông", + "LabelSettingsSquareBookCoversHelp": "Ưu tiên sử dụng ảnh bìa vuông hơn ảnh bìa tiêu chuẩn 1.6:1", + "LabelSettingsStoreCoversWithItem": "Lưu trữ ảnh bìa với mục", + "LabelSettingsStoreCoversWithItemHelp": "Theo mặc định, ảnh bìa được lưu trữ trong /metadata/items, bật cài đặt này sẽ lưu trữ ảnh bìa trong thư mục mục của thư viện bạn. Chỉ một tệp có tên là \"cover\" sẽ được giữ lại", + "LabelSettingsStoreMetadataWithItem": "Lưu trữ siêu dữ liệu với mục", + "LabelSettingsStoreMetadataWithItemHelp": "Theo mặc định, các tệp siêu dữ liệu được lưu trữ trong /metadata/items, bật cài đặt này sẽ lưu trữ các tệp siêu dữ liệu trong các thư mục mục của thư viện bạn", + "LabelSettingsTimeFormat": "Định dạng Thời gian", + "LabelShowAll": "Hiển thị Tất cả", + "LabelSize": "Kích thước", + "LabelSleepTimer": "Hẹn giờ tắt", + "LabelSlug": "Slug", + "LabelStart": "Bắt đầu", + "LabelStarted": "Đã bắt đầu", + "LabelStartedAt": "Bắt đầu vào", + "LabelStartTime": "Thời gian bắt đầu", + "LabelStatsAudioTracks": "Audio Tracks", + "LabelStatsAuthors": "Tác giả", + "LabelStatsBestDay": "Ngày tốt nhất", + "LabelStatsDailyAverage": "Trung bình hàng ngày", + "LabelStatsDays": "Ngày", + "LabelStatsDaysListened": "Ngày đã nghe", + "LabelStatsHours": "Giờ", + "LabelStatsInARow": "liên tiếp", + "LabelStatsItemsFinished": "Mục đã hoàn thành", + "LabelStatsItemsInLibrary": "Mục trong thư viện", + "LabelStatsMinutes": "phút", + "LabelStatsMinutesListening": "Phút Nghe", + "LabelStatsOverallDays": "Tổng số ngày", + "LabelStatsOverallHours": "Tổng số giờ", + "LabelStatsWeekListening": "Tuần nghe", + "LabelSubtitle": "Phụ đề", + "LabelSupportedFileTypes": "Loại tệp được hỗ trợ", + "LabelTag": "Thẻ", + "LabelTags": "Thẻ", + "LabelTagsAccessibleToUser": "Thẻ Có Thể Truy Cập Cho Người Dùng", + "LabelTagsNotAccessibleToUser": "Thẻ Không Thể Truy Cập Cho Người Dùng", + "LabelTasks": "Nhiệm vụ Đang chạy", + "LabelTextEditorBulletedList": "Danh sách có dấu đầu dòng", + "LabelTextEditorLink": "Liên kết", + "LabelTextEditorNumberedList": "Danh sách đánh số", + "LabelTextEditorUnlink": "Gỡ liên kết", + "LabelTheme": "Chủ đề", + "LabelThemeDark": "Tối", + "LabelThemeLight": "Sáng", + "LabelTimeBase": "Thời gian cơ bản", + "LabelTimeListened": "Thời gian đã nghe", + "LabelTimeListenedToday": "Thời gian đã nghe hôm nay", + "LabelTimeRemaining": "{0} còn lại", + "LabelTimeToShift": "Thời gian dời chuyển theo giây", + "LabelTitle": "Tiêu đề", + "LabelToolsEmbedMetadata": "Nhúng siêu dữ liệu", + "LabelToolsEmbedMetadataDescription": "Nhúng siêu dữ liệu vào tệp âm thanh bao gồm ảnh bìa và chương.", + "LabelToolsMakeM4b": "Tạo Tệp Audiobook M4B", + "LabelToolsMakeM4bDescription": "Tạo tệp audiobook .M4B với siêu dữ liệu nhúng, ảnh bìa và chương.", + "LabelToolsSplitM4b": "Chia M4B thành MP3", + "LabelToolsSplitM4bDescription": "Tạo MP3 từ M4B được chia theo chương với siêu dữ liệu nhúng, ảnh bìa và chương.", + "LabelTotalDuration": "Tổng thời lượng", + "LabelTotalTimeListened": "Tổng thời gian đã nghe", + "LabelTrackFromFilename": "Từ tên tệp", + "LabelTrackFromMetadata": "Từ siêu dữ liệu", + "LabelTracks": "Bài hát", + "LabelTracksMultiTrack": "Nhiều track", + "LabelTracksNone": "Không có track", + "LabelTracksSingleTrack": "Một track", + "LabelType": "Loại", + "LabelUnabridged": "Không rút gọn", + "LabelUndo": "Hoàn tác", + "LabelUnknown": "Không xác định", + "LabelUpdateCover": "Cập nhật ảnh bìa", + "LabelUpdateCoverHelp": "Cho phép ghi đè lên các ảnh bìa hiện có cho các cuốn sách được chọn khi tìm thấy một kết hợp", + "LabelUpdatedAt": "Cập nhật lúc", + "LabelUpdateDetails": "Cập nhật chi tiết", + "LabelUpdateDetailsHelp": "Cho phép ghi đè lên các chi tiết hiện có cho các cuốn sách được chọn khi tìm thấy một kết hợp", + "LabelUploaderDragAndDrop": "Kéo và thả tệp hoặc thư mục", + "LabelUploaderDropFiles": "Thả tệp", + "LabelUploaderItemFetchMetadataHelp": "Tự động lấy tiêu đề, tác giả và loạt", + "LabelUseChapterTrack": "Sử dụng track chương", + "LabelUseFullTrack": "Sử dụng toàn bộ track", + "LabelUser": "Người dùng", + "LabelUsername": "Tên người dùng", + "LabelValue": "Giá trị", + "LabelVersion": "Phiên bản", + "LabelViewBookmarks": "Xem các đánh dấu", + "LabelViewChapters": "Xem các chương", + "LabelViewQueue": "Xem hàng đợi phát", + "LabelVolume": "Âm lượng", + "LabelWeekdaysToRun": "Ngày trong tuần để chạy", + "LabelYearReviewHide": "Ẩn Năm trong Bài Đánh Giá", + "LabelYearReviewShow": "Xem Năm trong Bài Đánh Giá", + "LabelYourAudiobookDuration": "Thời lượng sách nói của bạn", + "LabelYourBookmarks": "Đánh dấu của bạn", + "LabelYourPlaylists": "Danh sách phát của bạn", + "LabelYourProgress": "Tiến trình của bạn", + "MessageAddToPlayerQueue": "Thêm vào hàng đợi phát", + "MessageAppriseDescription": "Để sử dụng tính năng này, bạn cần có một phiên bản của <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> đang chạy hoặc một api sẽ xử lý các yêu cầu tương tự. <br /> Địa chỉ URL của Apprise API nên là đường dẫn URL đầy đủ để gửi thông báo, ví dụ, nếu phiên bản API của bạn được phục vụ tại <code>http://192.168.1.1:8337</code> thì bạn sẽ đặt <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "Bản sao bao gồm người dùng, tiến độ của người dùng, chi tiết mục thư viện, cài đặt máy chủ và hình ảnh được lưu trữ trong <code>/metadata/items</code> & <code>/metadata/authors</code>. Bản sao <strong>không</strong> bao gồm bất kỳ tệp nào được lưu trữ trong các thư mục thư viện của bạn.", + "MessageBatchQuickMatchDescription": "Quick Match sẽ cố gắng thêm các ảnh bìa và siêu dữ liệu bị thiếu cho các mục đã chọn. Bật các tùy chọn dưới đây để cho phép Quick Match ghi đè lên các ảnh bìa hiện có và / hoặc siêu dữ liệu.", + "MessageBookshelfNoCollections": "Bạn chưa tạo bất kỳ bộ sưu tập nào", + "MessageBookshelfNoResultsForFilter": "Không có Kết quả cho bộ lọc \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "Không có nguồn cung cấp RSS nào đang mở", + "MessageBookshelfNoSeries": "Bạn không có bộ sách", + "MessageChapterEndIsAfter": "Kết thúc chương sau khi kết thúc sách nói của bạn", + "MessageChapterErrorFirstNotZero": "Chương đầu tiên phải bắt đầu từ 0", + "MessageChapterErrorStartGteDuration": "Thời gian bắt đầu không hợp lệ phải nhỏ hơn thời lượng sách nói", + "MessageChapterErrorStartLtPrev": "Thời gian bắt đầu không hợp lệ phải lớn hơn hoặc bằng thời gian bắt đầu của chương trước", + "MessageChapterStartIsAfter": "Bắt đầu chương sau khi kết thúc sách nói của bạn", + "MessageCheckingCron": "Kiểm tra cron...", + "MessageConfirmCloseFeed": "Bạn có chắc chắn muốn đóng nguồn cung cấp này không?", + "MessageConfirmDeleteBackup": "Bạn có chắc chắn muốn xóa bản sao lưu cho {0} không?", + "MessageConfirmDeleteFile": "Điều này sẽ xóa tệp khỏi hệ thống tệp của bạn. Bạn có chắc chắn không?", + "MessageConfirmDeleteLibrary": "Bạn có chắc chắn muốn xóa vĩnh viễn thư viện \"{0}\" không?", + "MessageConfirmDeleteLibraryItem": "Điều này sẽ xóa mục thư viện khỏi cơ sở dữ liệu và hệ thống tệp của bạn. Bạn có chắc chắn không?", + "MessageConfirmDeleteLibraryItems": "Điều này sẽ xóa {0} mục thư viện khỏi cơ sở dữ liệu và hệ thống tệp của bạn. Bạn có chắc chắn không?", + "MessageConfirmDeleteSession": "Bạn có chắc chắn muốn xóa phiên này không?", + "MessageConfirmForceReScan": "Bạn có chắc chắn muốn buộc quét lại không?", + "MessageConfirmMarkAllEpisodesFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các tập phim đã kết thúc không?", + "MessageConfirmMarkAllEpisodesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các tập phim chưa kết thúc không?", + "MessageConfirmMarkSeriesFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này đã kết thúc không?", + "MessageConfirmMarkSeriesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này chưa kết thúc không?", + "MessageConfirmQuickEmbed": "Cảnh báo! Quick embed sẽ không sao lưu các tệp âm thanh của bạn. Đảm bảo bạn có một bản sao lưu của các tệp âm thanh của bạn. <br><br>Bạn có muốn tiếp tục không?", + "MessageConfirmRemoveAllChapters": "Bạn có chắc chắn muốn xóa tất cả các chương không?", + "MessageConfirmRemoveAuthor": "Bạn có chắc chắn muốn xóa tác giả \"{0}\" không?", + "MessageConfirmRemoveCollection": "Bạn có chắc chắn muốn xóa bộ sưu tập \"{0}\" không?", + "MessageConfirmRemoveEpisode": "Bạn có chắc chắn muốn xóa tập phim \"{0}\" không?", + "MessageConfirmRemoveEpisodes": "Bạn có chắc chắn muốn xóa {0} tập phim không?", + "MessageConfirmRemoveListeningSessions": "Bạn có chắc chắn muốn xóa {0} phiên nghe không?", + "MessageConfirmRemoveNarrator": "Bạn có chắc chắn muốn xóa người kể chuyện \"{0}\" không?", + "MessageConfirmRemovePlaylist": "Bạn có chắc chắn muốn xóa danh sách phát của bạn \"{0}\" không?", + "MessageConfirmRenameGenre": "Bạn có chắc chắn muốn đổi tên thể loại \"{0}\" thành \"{1}\" cho tất cả các mục không?", + "MessageConfirmRenameGenreMergeNote": "Lưu ý: Thể loại này đã tồn tại nên chúng sẽ được hợp nhất.", + "MessageConfirmRenameGenreWarning": "Cảnh báo! Một thể loại tương tự với kiểu chữ khác đã tồn tại \"{0}\".", + "MessageConfirmRenameTag": "Bạn có chắc chắn muốn đổi tên tag \"{0}\" thành \"{1}\" cho tất cả các mục không?", + "MessageConfirmRenameTagMergeNote": "Lưu ý: Thẻ này đã tồn tại nên chúng sẽ được hợp nhất.", + "MessageConfirmRenameTagWarning": "Cảnh báo! Một thẻ tương tự với kiểu chữ khác đã tồn tại \"{0}\".", + "MessageConfirmReScanLibraryItems": "Bạn có chắc chắn muốn quét lại {0} mục không?", + "MessageConfirmSendEbookToDevice": "Bạn có chắc chắn muốn gửi {0} ebook \"{1}\" đến thiết bị \"{2}\" không?", + "MessageDownloadingEpisode": "Đang tải tập phim", + "MessageDragFilesIntoTrackOrder": "Kéo tệp vào thứ tự track đúng", + "MessageEmbedFinished": "Nhúng Hoàn thành!", + "MessageEpisodesQueuedForDownload": "{0} Tập(s) đã được thêm vào hàng đợi để tải xuống", + "MessageFeedURLWillBe": "URL nguồn cấp sẽ là {0}", + "MessageFetching": "Đang tìm...", + "MessageForceReScanDescription": "sẽ quét lại tất cả các tệp như một quét mới. Các thẻ ID3 của tệp âm thanh, tệp OPF và tệp văn bản sẽ được quét làm mới.", + "MessageImportantNotice": "Thông báo quan trọng!", + "MessageInsertChapterBelow": "Chèn chương dưới đây", + "MessageItemsSelected": "{0} Mục Đã Chọn", + "MessageItemsUpdated": "{0} Mục Đã Cập Nhật", + "MessageJoinUsOn": "Tham gia cùng chúng tôi trên", + "MessageListeningSessionsInTheLastYear": "{0} phiên nghe trong năm qua", + "MessageLoading": "Đang tải...", + "MessageLoadingFolders": "Đang tải các thư mục...", + "MessageM4BFailed": "M4B thất bại!", + "MessageM4BFinished": "M4B Hoàn thành!", + "MessageMapChapterTitles": "Ánh xạ tiêu đề chương với các chương hiện có của sách audio của bạn mà không điều chỉnh thời gian", + "MessageMarkAllEpisodesFinished": "Đánh dấu tất cả các tập phim đã kết thúc", + "MessageMarkAllEpisodesNotFinished": "Đánh dấu tất cả các tập phim chưa kết thúc", + "MessageMarkAsFinished": "Đánh dấu là Đã Kết Thúc", + "MessageMarkAsNotFinished": "Đánh dấu là Chưa Kết Thúc", + "MessageMatchBooksDescription": "sẽ cố gắng kết hợp các sách trong thư viện với một cuốn sách từ nhà cung cấp tìm kiếm được chọn và điền vào các chi tiết trống và ảnh bìa. Không ghi đè các chi tiết.", + "MessageNoAudioTracks": "Không có track âm thanh", + "MessageNoAuthors": "Không có Tác giả", + "MessageNoBackups": "Không có Bản sao lưu", + "MessageNoBookmarks": "Không có Đánh dấu", + "MessageNoChapters": "Không có Chương", + "MessageNoCollections": "Không có Bộ sưu tập", + "MessageNoCoversFound": "Không tìm thấy Ảnh bìa", + "MessageNoDescription": "Không có mô tả", + "MessageNoDownloadsInProgress": "Không có tải xuống đang tiến hành", + "MessageNoDownloadsQueued": "Không có tải xuống được xếp hàng", + "MessageNoEpisodeMatchesFound": "Không tìm thấy tập phim nào phù hợp", + "MessageNoEpisodes": "Không có Tập phim", + "MessageNoFoldersAvailable": "Không có Thư mục nào có sẵn", + "MessageNoGenres": "Không có Thể loại", + "MessageNoIssues": "Không có Vấn đề", + "MessageNoItems": "Không có Mục", + "MessageNoItemsFound": "Không tìm thấy mục nào", + "MessageNoListeningSessions": "Không có Phiên Nghe", + "MessageNoLogs": "Không có Log", + "MessageNoMediaProgress": "Không có Tiến độ Phương tiện", + "MessageNoNotifications": "Không có Thông báo", + "MessageNoPodcastsFound": "Không tìm thấy podcast nào", + "MessageNoResults": "Không có Kết quả", + "MessageNoSearchResultsFor": "Không có kết quả tìm kiếm cho \"{0}\"", + "MessageNoSeries": "Không có Bộ", + "MessageNoTags": "Không có Thẻ", + "MessageNoTasksRunning": "Không có Công việc đang chạy", + "MessageNotYetImplemented": "Chưa được triển khai", + "MessageNoUpdateNecessary": "Không cần cập nhật", + "MessageNoUpdatesWereNecessary": "Không cần cập nhật", + "MessageNoUserPlaylists": "Bạn chưa có danh sách phát", + "MessageOr": "hoặc", + "MessagePauseChapter": "Tạm dừng phát chương", + "MessagePlayChapter": "Nghe từ đầu chương", + "MessagePlaylistCreateFromCollection": "Tạo danh sách phát từ bộ sưu tập", + "MessagePodcastHasNoRSSFeedForMatching": "Podcast không có RSS feed để sử dụng cho việc kết hợp", + "MessageQuickMatchDescription": "Điền chi tiết mục trống và ảnh bìa với kết quả phù hợp đầu tiên từ '{0}'. Không ghi đè chi tiết trừ khi cài đặt máy chủ 'Ưu tiên dữ liệu phù hợp' được bật.", + "MessageRemoveChapter": "Xóa chương", + "MessageRemoveEpisodes": "Xóa {0} tập", + "MessageRemoveFromPlayerQueue": "Xóa khỏi hàng đợi phát", + "MessageRemoveUserWarning": "Bạn có chắc chắn muốn xóa người dùng \"{0}\" một cách vĩnh viễn không?", + "MessageReportBugsAndContribute": "Báo cáo lỗi, yêu cầu tính năng và đóng góp tại", + "MessageResetChaptersConfirm": "Bạn có chắc chắn muốn đặt lại các chương và hủy những thay đổi bạn đã thực hiện không?", + "MessageRestoreBackupConfirm": "Bạn có chắc chắn muốn khôi phục bản sao lưu được tạo vào", + "MessageRestoreBackupWarning": "Việc khôi phục bản sao lưu sẽ ghi đè lên toàn bộ cơ sở dữ liệu được đặt tại /config và ảnh bìa trong /metadata/items & /metadata/authors.<br /><br />Bản sao lưu không sửa đổi bất kỳ tệp nào trong các thư mục thư viện của bạn. Nếu bạn đã bật các cài đặt máy chủ để lưu ảnh bìa và dữ liệu phần mềm trong các thư mục thư viện của mình thì chúng sẽ không được sao lưu hoặc ghi đè.<br /><br />Tất cả các máy khách sử dụng máy chủ của bạn sẽ được làm mới tự động.", + "MessageSearchResultsFor": "Kết quả tìm kiếm cho", + "MessageSelected": "{0} đã được chọn", + "MessageServerCouldNotBeReached": "Không thể kết nối đến máy chủ", + "MessageSetChaptersFromTracksDescription": "Đặt chương sử dụng mỗi tệp âm thanh là một chương và tiêu đề chương là tên tệp âm thanh", + "MessageStartPlaybackAtTime": "Bắt đầu phát \"{0}\" tại thời điểm {1}?", + "MessageThinking": "Đang suy nghĩ...", + "MessageUploaderItemFailed": "Không thể tải lên", + "MessageUploaderItemSuccess": "Tải lên thành công!", + "MessageUploading": "Đang tải lên...", + "MessageValidCronExpression": "Biểu thức cron hợp lệ", + "MessageWatcherIsDisabledGlobally": "Watcher đã bị vô hiệu hóa toàn cầu trong cài đặt máy chủ", + "MessageXLibraryIsEmpty": "Thư viện {0} rỗng!", + "MessageYourAudiobookDurationIsLonger": "Thời lượng sách nói của bạn dài hơn so với thời lượng tìm thấy", + "MessageYourAudiobookDurationIsShorter": "Thời lượng sách nói của bạn ngắn hơn so với thời lượng tìm thấy", + "NoteChangeRootPassword": "Người dùng Root là người dùng duy nhất có thể có mật khẩu trống", + "NoteChapterEditorTimes": "Lưu ý: Thời gian bắt đầu của chương đầu tiên phải ở 0:00 và thời gian bắt đầu của chương cuối cùng không thể vượt quá thời lượng của sách nói này.", + "NoteFolderPicker": "Lưu ý: các thư mục đã được ánh xạ trước đó sẽ không được hiển thị", + "NoteRSSFeedPodcastAppsHttps": "Cảnh báo: Hầu hết các ứng dụng podcast sẽ yêu cầu URL của RSS feed sử dụng HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "Cảnh báo: 1 hoặc nhiều tập của bạn không có Pub Date. Một số ứng dụng podcast yêu cầu điều này.", + "NoteUploaderFoldersWithMediaFiles": "Các thư mục có tệp phương tiện sẽ được xử lý như các mục thư viện riêng biệt.", + "NoteUploaderOnlyAudioFiles": "Nếu chỉ tải lên các tệp âm thanh thì mỗi tệp âm thanh sẽ được xử lý như một cuốn sách nói riêng biệt.", + "NoteUploaderUnsupportedFiles": "Các tệp không được hỗ trợ sẽ bị bỏ qua. Khi chọn hoặc thả một thư mục, các tệp khác không có trong thư mục mục sẽ bị bỏ qua.", + "PlaceholderNewCollection": "Tên bộ sưu tập mới", + "PlaceholderNewFolderPath": "Đường dẫn thư mục mới", + "PlaceholderNewPlaylist": "Tên danh sách phát mới", + "PlaceholderSearch": "Tìm kiếm..", + "PlaceholderSearchEpisode": "Tìm kiếm tập..", + "ToastAccountUpdateFailed": "Cập nhật tài khoản thất bại", + "ToastAccountUpdateSuccess": "Tài khoản đã được cập nhật", + "ToastAuthorImageRemoveFailed": "Không thể xóa ảnh tác giả", + "ToastAuthorImageRemoveSuccess": "Ảnh tác giả đã được xóa", + "ToastAuthorUpdateFailed": "Cập nhật tác giả thất bại", + "ToastAuthorUpdateMerged": "Tác giả đã được hợp nhất", + "ToastAuthorUpdateSuccess": "Cập nhật tác giả thành công", + "ToastAuthorUpdateSuccessNoImageFound": "Cập nhật tác giả thành công (không tìm thấy ảnh)", + "ToastBackupCreateFailed": "Tạo bản sao lưu thất bại", + "ToastBackupCreateSuccess": "Bản sao lưu được tạo", + "ToastBackupDeleteFailed": "Xóa bản sao lưu thất bại", + "ToastBackupDeleteSuccess": "Bản sao lưu đã được xóa", + "ToastBackupRestoreFailed": "Khôi phục bản sao lưu thất bại", + "ToastBackupUploadFailed": "Tải lên bản sao lưu thất bại", + "ToastBackupUploadSuccess": "Bản sao lưu đã được tải lên", + "ToastBatchUpdateFailed": "Cập nhật nhóm thất bại", + "ToastBatchUpdateSuccess": "Cập nhật nhóm thành công", + "ToastBookmarkCreateFailed": "Tạo đánh dấu thất bại", + "ToastBookmarkCreateSuccess": "Đã thêm đánh dấu", + "ToastBookmarkRemoveFailed": "Xóa đánh dấu thất bại", + "ToastBookmarkRemoveSuccess": "Đánh dấu đã được xóa", + "ToastBookmarkUpdateFailed": "Cập nhật đánh dấu thất bại", + "ToastBookmarkUpdateSuccess": "Đánh dấu đã được cập nhật", + "ToastChaptersHaveErrors": "Các chương có lỗi", + "ToastChaptersMustHaveTitles": "Các chương phải có tiêu đề", + "ToastCollectionItemsRemoveFailed": "Xóa mục từ bộ sưu tập thất bại", + "ToastCollectionItemsRemoveSuccess": "Mục đã được xóa khỏi bộ sưu tập", + "ToastCollectionRemoveFailed": "Xóa bộ sưu tập thất bại", + "ToastCollectionRemoveSuccess": "Bộ sưu tập đã được xóa", + "ToastCollectionUpdateFailed": "Cập nhật bộ sưu tập thất bại", + "ToastCollectionUpdateSuccess": "Bộ sưu tập đã được cập nhật", + "ToastItemCoverUpdateFailed": "Cập nhật ảnh bìa mục thất bại", + "ToastItemCoverUpdateSuccess": "Ảnh bìa mục đã được cập nhật", + "ToastItemDetailsUpdateFailed": "Cập nhật chi tiết mục thất bại", + "ToastItemDetailsUpdateSuccess": "Chi tiết mục đã được cập nhật", + "ToastItemDetailsUpdateUnneeded": "Không cần cập nhật chi tiết mục", + "ToastItemMarkedAsFinishedFailed": "Đánh dấu mục là Hoàn thành thất bại", + "ToastItemMarkedAsFinishedSuccess": "Mục đã được đánh dấu là Hoàn thành", + "ToastItemMarkedAsNotFinishedFailed": "Đánh dấu mục là Chưa hoàn thành thất bại", + "ToastItemMarkedAsNotFinishedSuccess": "Mục đã được đánh dấu là Chưa hoàn thành", + "ToastLibraryCreateFailed": "Tạo thư viện thất bại", + "ToastLibraryCreateSuccess": "Thư viện \"{0}\" đã được tạo", + "ToastLibraryDeleteFailed": "Xóa thư viện thất bại", + "ToastLibraryDeleteSuccess": "Thư viện đã được xóa", + "ToastLibraryScanFailedToStart": "Không thể bắt đầu quét thư viện", + "ToastLibraryScanStarted": "Quét thư viện đã được bắt đầu", + "ToastLibraryUpdateFailed": "Cập nhật thư viện thất bại", + "ToastLibraryUpdateSuccess": "Thư viện \"{0}\" đã được cập nhật", + "ToastPlaylistCreateFailed": "Tạo danh sách phát thất bại", + "ToastPlaylistCreateSuccess": "Danh sách phát đã được tạo", + "ToastPlaylistRemoveFailed": "Xóa danh sách phát thất bại", + "ToastPlaylistRemoveSuccess": "Danh sách phát đã được xóa", + "ToastPlaylistUpdateFailed": "Cập nhật danh sách phát thất bại", + "ToastPlaylistUpdateSuccess": "Danh sách phát đã được cập nhật", + "ToastPodcastCreateFailed": "Tạo podcast thất bại", + "ToastPodcastCreateSuccess": "Podcast đã được tạo thành công", + "ToastRemoveItemFromCollectionFailed": "Xóa mục khỏi bộ sưu tập thất bại", + "ToastRemoveItemFromCollectionSuccess": "Mục đã được xóa khỏi bộ sưu tập", + "ToastRSSFeedCloseFailed": "Đóng nguồn cấp RSS thất bại", + "ToastRSSFeedCloseSuccess": "Nguồn cấp RSS đã được đóng", + "ToastSendEbookToDeviceFailed": "Gửi ebook đến thiết bị thất bại", + "ToastSendEbookToDeviceSuccess": "Ebook đã được gửi đến thiết bị \"{0}\"", + "ToastSeriesUpdateFailed": "Cập nhật loạt truyện thất bại", + "ToastSeriesUpdateSuccess": "Cập nhật loạt truyện thành công", + "ToastSessionDeleteFailed": "Xóa phiên thất bại", + "ToastSessionDeleteSuccess": "Phiên đã được xóa", + "ToastSocketConnected": "Kết nối socket", + "ToastSocketDisconnected": "Ngắt kết nối socket", + "ToastSocketFailedToConnect": "Không thể kết nối socket", + "ToastUserDeleteFailed": "Xóa người dùng thất bại", + "ToastUserDeleteSuccess": "Người dùng đã được xóa" +} \ No newline at end of file From e0dddae2c2d88ab45c5af49c04550a5e30d40006 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Sat, 9 Mar 2024 23:13:20 +0000 Subject: [PATCH 0424/2145] Added missing keys --- client/strings/vi-vn.json | 1528 +++++++++++++++++++------------------ 1 file changed, 778 insertions(+), 750 deletions(-) diff --git a/client/strings/vi-vn.json b/client/strings/vi-vn.json index c150cc7e..8c84a8c0 100644 --- a/client/strings/vi-vn.json +++ b/client/strings/vi-vn.json @@ -1,751 +1,779 @@ { - "ButtonAdd": "Thêm", - "ButtonAddChapters": "Thêm Chương", - "ButtonAddDevice": "Thêm Thiết Bị", - "ButtonAddLibrary": "Thêm Thư Viện", - "ButtonAddPodcasts": "Thêm Podcasts", - "ButtonAddUser": "Thêm Người Dùng", - "ButtonAddYourFirstLibrary": "Thêm thư viện đầu tiên của bạn", - "ButtonApply": "Áp Dụng", - "ButtonApplyChapters": "Áp Dụng Chương", - "ButtonAuthors": "Tác Giả", - "ButtonBrowseForFolder": "Duyệt Thư Mục", - "ButtonCancel": "Hủy", - "ButtonCancelEncode": "Hủy Mã Hóa", - "ButtonChangeRootPassword": "Thay Đổi Mật Khẩu Root", - "ButtonCheckAndDownloadNewEpisodes": "Kiểm Tra và Tải Xuống Các Tập Phim Mới", - "ButtonChooseAFolder": "Chọn một thư mục", - "ButtonChooseFiles": "Chọn tập tin", - "ButtonClearFilter": "Xóa Bộ Lọc", - "ButtonCloseFeed": "Đóng Feed", - "ButtonCollections": "Bộ Sưu Tập", - "ButtonConfigureScanner": "Cấu Hình Bộ Quét", - "ButtonCreate": "Tạo", - "ButtonCreateBackup": "Tạo Bản Sao Lưu", - "ButtonDelete": "Xóa", - "ButtonDownloadQueue": "Hàng Chờ", - "ButtonEdit": "Chỉnh Sửa", - "ButtonEditChapters": "Chỉnh Sửa Chương", - "ButtonEditPodcast": "Chỉnh Sửa Podcast", - "ButtonForceReScan": "Force Re-Scan", - "ButtonFullPath": "Đường Dẫn Đầy Đủ", - "ButtonHide": "Ẩn", - "ButtonHome": "Trang Chủ", - "ButtonIssues": "Vấn Đề", - "ButtonJumpBackward": "Bước Lùi", - "ButtonJumpForward": "Bước Tiến", - "ButtonLatest": "Mới Nhất", - "ButtonLibrary": "Thư Viện", - "ButtonLogout": "Đăng Xuất", - "ButtonLookup": "Tra Cứu", - "ButtonManageTracks": "Quản Lý Tracks", - "ButtonMapChapterTitles": "Ánh Xạ Tiêu Đề Chương", - "ButtonMatchAllAuthors": "Khớp Tất Cả Tác Giả", - "ButtonMatchBooks": "Khớp Sách", - "ButtonNevermind": "Không Sao", - "ButtonNext": "Tiếp Theo", - "ButtonNextChapter": "Chương Tiếp Theo", - "ButtonOk": "Ok", - "ButtonOpenFeed": "Mở Feed", - "ButtonOpenManager": "Mở Quản Lý", - "ButtonPause": "Tạm Dừng", - "ButtonPlay": "Phát", - "ButtonPlaying": "Đang Phát", - "ButtonPlaylists": "Danh Sách Phát", - "ButtonPrevious": "Trước", - "ButtonPreviousChapter": "Chương Trước", - "ButtonPurgeAllCache": "Xóa Sạch Tất Cả Bộ Nhớ Cache", - "ButtonPurgeItemsCache": "Xóa Sạch Bộ Nhớ Cache Các Mục", - "ButtonPurgeMediaProgress": "Xóa Sạch Tiến Trình Phương Tiện", - "ButtonQueueAddItem": "Thêm vào hàng đợi", - "ButtonQueueRemoveItem": "Xóa khỏi hàng đợi", - "ButtonQuickMatch": "Khớp Nhanh", - "ButtonRead": "Đọc", - "ButtonRefresh": "Làm Mới", - "ButtonRemove": "Xóa", - "ButtonRemoveAll": "Xóa Tất Cả", - "ButtonRemoveAllLibraryItems": "Xóa Tất Cả Các Mục Thư Viện", - "ButtonRemoveFromContinueListening": "Xóa khỏi Tiếp Tục Nghe", - "ButtonRemoveFromContinueReading": "Xóa khỏi Tiếp Tục Đọc", - "ButtonRemoveSeriesFromContinueSeries": "Xóa Series khỏi Tiếp Tục Series", - "ButtonReScan": "Quét Lại", - "ButtonReset": "Đặt Lại", - "ButtonResetToDefault": "Đặt Lại về Mặc Định", - "ButtonRestore": "Khôi Phục", - "ButtonSave": "Lưu", - "ButtonSaveAndClose": "Lưu & Đóng", - "ButtonSaveTracklist": "Lưu Danh Sách Track", - "ButtonScan": "Quét", - "ButtonScanLibrary": "Quét Thư Viện", - "ButtonSearch": "Tìm Kiếm", - "ButtonSelectFolderPath": "Chọn Đường Dẫn Thư Mục", - "ButtonSeries": "Series", - "ButtonSetChaptersFromTracks": "Đặt chương từ các track", - "ButtonShare": "Chia Sẻ", - "ButtonShiftTimes": "Dời Thời Gian", - "ButtonShow": "Hiện", - "ButtonStartM4BEncode": "Bắt đầu Mã Hóa M4B", - "ButtonStartMetadataEmbed": "Bắt đầu Nhúng Dữ Liệu", - "ButtonSubmit": "Gửi", - "ButtonTest": "Kiểm Tra", - "ButtonUpload": "Tải Lên", - "ButtonUploadBackup": "Tải Lên Bản Sao Lưu", - "ButtonUploadCover": "Tải Lên Bìa", - "ButtonUploadOPMLFile": "Tải Lên Tệp OPML", - "ButtonUserDelete": "Xóa người dùng {0}", - "ButtonUserEdit": "Chỉnh Sửa người dùng {0}", - "ButtonViewAll": "Xem Tất Cả", - "ButtonYes": "Có", - "ErrorUploadFetchMetadataAPI": "Lỗi khi lấy dữ liệu metadata", - "ErrorUploadFetchMetadataNoResults": "Không thể lấy dữ liệu metadata - hãy thử cập nhật tiêu đề và/hoặc tác giả", - "ErrorUploadLacksTitle": "Phải có một tiêu đề", - "HeaderAccount": "Tài Khoản", - "HeaderAdvanced": "Nâng Cao", - "HeaderAppriseNotificationSettings": "Cài Đặt Thông Báo Apprise", - "HeaderAudiobookTools": "Công Cụ Quản Lý Tệp Truyện Nói", - "HeaderAudioTracks": "Các Track Âm Thanh", - "HeaderAuthentication": "Xác Thực", - "HeaderBackups": "Bản Sao Lưu", - "HeaderChangePassword": "Thay Đổi Mật Khẩu", - "HeaderChapters": "Chương", - "HeaderChooseAFolder": "Chọn Một Thư Mục", - "HeaderCollection": "Bộ Sưu Tập", - "HeaderCollectionItems": "Các Mục Bộ Sưu Tập", - "HeaderCover": "Bìa", - "HeaderCurrentDownloads": "Tải Xuống Hiện Tại", - "HeaderCustomMetadataProviders": "Các Nhà Cung Cấp Metadata Tùy Chỉnh", - "HeaderDetails": "Chi Tiết", - "HeaderDownloadQueue": "Hàng Đợi Tải Xuống", - "HeaderEbookFiles": "Tệp Ebook", - "HeaderEmail": "Email", - "HeaderEmailSettings": "Cài Đặt Email", - "HeaderEpisodes": "Tập Phim", - "HeaderEreaderDevices": "Thiết Bị Đọc Sách", - "HeaderEreaderSettings": "Cài Đặt Thiết Bị Đọc Sách", - "HeaderFiles": "Tệp", - "HeaderFindChapters": "Tìm Kiếm Chương", - "HeaderIgnoredFiles": "Tệp Bị Bỏ Qua", - "HeaderItemFiles": "Tệp Mục", - "HeaderItemMetadataUtils": "Công Cụ Metadata Mục", - "HeaderLastListeningSession": "Phiên Nghe Gần Nhất", - "HeaderLatestEpisodes": "Tập Mới Nhất", - "HeaderLibraries": "Thư Viện", - "HeaderLibraryFiles": "Tệp Thư Viện", - "HeaderLibraryStats": "Thống Kê Thư Viện", - "HeaderListeningSessions": "Phiên Nghe", - "HeaderListeningStats": "Thống Kê Nghe", - "HeaderLogin": "Đăng Nhập", - "HeaderLogs": "Nhật Ký", - "HeaderManageGenres": "Quản Lý Thể Loại", - "HeaderManageTags": "Quản Lý Thẻ", - "HeaderMapDetails": "Bản Đồ Chi Tiết", - "HeaderMatch": "Kết Hợp", - "HeaderMetadataOrderOfPrecedence": "Thứ Tự Ưu Tiên Metadata", - "HeaderMetadataToEmbed": "Metadata để nhúng", - "HeaderNewAccount": "Tài Khoản Mới", - "HeaderNewLibrary": "Thư Viện Mới", - "HeaderNotifications": "Thông Báo", - "HeaderOpenIDConnectAuthentication": "Xác Thực Mở ID Connect", - "HeaderOpenRSSFeed": "Mở RSS Feed", - "HeaderOtherFiles": "Các Tệp Khác", - "HeaderPasswordAuthentication": "Xác Thực Mật Khẩu", - "HeaderPermissions": "Quyền Hạn", - "HeaderPlayerQueue": "Hàng Đợi Người Chơi", - "HeaderPlaylist": "Danh Sách Phát", - "HeaderPlaylistItems": "Các Mục Danh Sách Phát", - "HeaderPodcastsToAdd": "Podcasts để Thêm", - "HeaderPreviewCover": "Xem Trước Bìa", - "HeaderRemoveEpisode": "Xóa Tập", - "HeaderRemoveEpisodes": "Xóa {0} Tập", - "HeaderRSSFeedGeneral": "Chi Tiết RSS", - "HeaderRSSFeedIsOpen": "RSS Feed Đã Mở", - "HeaderRSSFeeds": "RSS Feeds", - "HeaderSavedMediaProgress": "Tiến Trình Phương Tiện Đã Lưu", - "HeaderSchedule": "Lịch Trình", - "HeaderScheduleLibraryScans": "Lên Lịch Quét Tự Động Thư Viện", - "HeaderSession": "Phiên", - "HeaderSetBackupSchedule": "Đặt Lịch Sao Lưu", - "HeaderSettings": "Cài Đặt", - "HeaderSettingsDisplay": "Hiển Thị", - "HeaderSettingsExperimental": "Tính Năng Thử Nghiệm", - "HeaderSettingsGeneral": "Chung", - "HeaderSettingsScanner": "Máy Quét", - "HeaderSleepTimer": "Hẹn Giờ Tắt", - "HeaderStatsLargestItems": "Các Mục Lớn Nhất", - "HeaderStatsLongestItems": "Các Mục Dài Nhất (giờ)", - "HeaderStatsMinutesListeningChart": "Thống Kê Thời Gian Nghe (7 ngày gần nhất)", - "HeaderStatsRecentSessions": "Các Phiên Gần Đây", - "HeaderStatsTop10Authors": "10 Tác Giả Hàng Đầu", - "HeaderStatsTop5Genres": "5 Thể Loại Hàng Đầu", - "HeaderTableOfContents": "Mục Lục", - "HeaderTools": "Công Cụ", - "HeaderUpdateAccount": "Cập Nhật Tài Khoản", - "HeaderUpdateAuthor": "Cập Nhật Tác Giả", - "HeaderUpdateDetails": "Cập Nhật Chi Tiết", - "HeaderUpdateLibrary": "Cập Nhật Thư Viện", - "HeaderUsers": "Người Dùng", - "HeaderYearReview": "Năm {0} trong Xem Xét", - "HeaderYourStats": "Thống Kê Của Bạn", - "LabelAbridged": "Rút Gọn", - "LabelAccountType": "Loại Tài Khoản", - "LabelAccountTypeAdmin": "Quản Trị Viên", - "LabelAccountTypeGuest": "Khách", - "LabelAccountTypeUser": "Người Dùng", - "LabelActivity": "Hoạt Động", - "LabelAdded": "Đã Thêm", - "LabelAddedAt": "Đã Thêm Lúc", - "LabelAddToCollection": "Thêm vào Bộ Sưu Tập", - "LabelAddToCollectionBatch": "Thêm {0} Sách vào Bộ Sưu Tập", - "LabelAddToPlaylist": "Thêm vào Danh Sách Phát", - "LabelBooks": "Sách", - "LabelButtonText": "Nút Văn Bản", - "LabelChangePassword": "Đổi Mật Khẩu", - "LabelChannels": "Kênh", - "LabelChapters": "Chương", - "LabelChaptersFound": "chương được tìm thấy", - "LabelChapterTitle": "Tiêu đề Chương", - "LabelClickForMoreInfo": "Nhấn để biết thêm thông tin", - "LabelClosePlayer": "Đóng trình phát", - "LabelCodec": "Mã hóa", - "LabelCollapseSeries": "Thu gọn Series", - "LabelCollection": "Bộ Sưu Tập", - "LabelCollections": "Các Bộ Sưu Tập", - "LabelComplete": "Hoàn Thành", - "LabelConfirmPassword": "Xác Nhận Mật Khẩu", - "LabelContinueListening": "Tiếp Tục Nghe", - "LabelContinueReading": "Tiếp Tục Đọc", - "LabelContinueSeries": "Tiếp Tục Series", - "LabelCover": "Bìa", - "LabelCoverImageURL": "URL Ảnh Bìa", - "LabelCreatedAt": "Được Tạo Lúc", - "LabelCronExpression": "Biểu Thức Cron", - "LabelCurrent": "Hiện tại", - "LabelCurrently": "Hiện tại:", - "LabelCustomCronExpression": "Biểu Thức Cron Tùy Chỉnh:", - "LabelDatetime": "Ngày giờ", - "LabelDeleteFromFileSystemCheckbox": "Xóa khỏi hệ thống tệp (bỏ chọn để chỉ xóa khỏi cơ sở dữ liệu)", - "LabelDescription": "Mô Tả", - "LabelDeselectAll": "Bỏ Chọn Tất Cả", - "LabelDevice": "Thiết Bị", - "LabelDeviceInfo": "Thông Tin Thiết Bị", - "LabelDeviceIsAvailableTo": "Thiết Bị Đã Sẵn Sàng Cho...", - "LabelDirectory": "Thư Mục", - "LabelDiscFromFilename": "Đĩa từ Tên Tệp", - "LabelDiscFromMetadata": "Đĩa từ Metadata", - "LabelDiscover": "Khám Phá", - "LabelDownload": "Tải Xuống", - "LabelDownloadNEpisodes": "Tải Xuống {0} Tập", - "LabelDuration": "Thời Lượng", - "LabelDurationFound": "Thời lượng được tìm thấy:", - "LabelEbook": "Ebook", - "LabelEbooks": "Các Ebook", - "LabelEdit": "Chỉnh Sửa", - "LabelEmail": "Email", - "LabelEmailSettingsFromAddress": "Địa chỉ Gửi từ", - "LabelEmailSettingsSecure": "Bảo Mật", - "LabelEmailSettingsSecureHelp": "Nếu đúng thì kết nối sẽ sử dụng TLS khi kết nối đến máy chủ. Nếu sai thì TLS sẽ được sử dụng nếu máy chủ hỗ trợ phần mở rộng STARTTLS. Trong hầu hết các trường hợp, hãy đặt giá trị này là đúng nếu bạn kết nối đến cổng 465. Đối với cổng 587 hoặc 25, giữ nó sai. (từ nodemailer.com/smtp/#authentication)", - "LabelEmailSettingsTestAddress": "Địa Chỉ Kiểm Tra", - "LabelEmbeddedCover": "Bìa Nội", - "LabelEnable": "Bật", - "LabelEnd": "Kết Thúc", - "LabelEpisode": "Tập", - "LabelEpisodeTitle": "Tiêu Đề Tập", - "LabelEpisodeType": "Loại Tập", - "LabelExample": "Ví Dụ", - "LabelExplicit": "Rõ Ràng", - "LabelFeedURL": "URL Feed", - "LabelFetchingMetadata": "Đang Lấy Metadata", - "LabelFile": "Tệp", - "LabelFileBirthtime": "Thời Gian Tạo Tệp", - "LabelFileModified": "Sửa Đổi Tệp", - "LabelFilename": "Tên Tệp", - "LabelFilterByUser": "Lọc theo Người Dùng", - "LabelFindEpisodes": "Tìm Tập", - "LabelFinished": "Hoàn Thành", - "LabelFolder": "Thư Mục", - "LabelFolders": "Các Thư Mục", - "LabelFontBold": "Đậm", - "LabelFontFamily": "Gia đình font", - "LabelFontItalic": "Nghiêng", - "LabelFontScale": "Tỷ lệ font", - "LabelFontStrikethrough": "Gạch ngang", - "LabelFormat": "Định dạng", - "LabelGenre": "Thể loại", - "LabelGenres": "Các thể loại", - "LabelHardDeleteFile": "Xóa tập tin vĩnh viễn", - "LabelHasEbook": "Có ebook", - "LabelHasSupplementaryEbook": "Có ebook bổ sung", - "LabelHighestPriority": "Ưu tiên cao nhất", - "LabelHost": "Máy chủ", - "LabelHour": "Giờ", - "LabelIcon": "Biểu tượng", - "LabelImageURLFromTheWeb": "URL hình ảnh từ web", - "LabelIncludeInTracklist": "Bao gồm trong danh sách phát", - "LabelIncomplete": "Chưa hoàn thành", - "LabelInProgress": "Đang tiến hành", - "LabelInterval": "Khoảng cách", - "LabelIntervalCustomDailyWeekly": "Tuỳ chỉnh hàng ngày/hàng tuần", - "LabelIntervalEvery12Hours": "Mỗi 12 giờ", - "LabelIntervalEvery15Minutes": "Mỗi 15 phút", - "LabelIntervalEvery2Hours": "Mỗi 2 giờ", - "LabelIntervalEvery30Minutes": "Mỗi 30 phút", - "LabelIntervalEvery6Hours": "Mỗi 6 giờ", - "LabelIntervalEveryDay": "Mỗi ngày", - "LabelIntervalEveryHour": "Mỗi giờ", - "LabelInvalidParts": "Phần không hợp lệ", - "LabelInvert": "Nghịch đảo", - "LabelItem": "Mục", - "LabelLanguage": "Ngôn ngữ", - "LabelLanguageDefaultServer": "Ngôn ngữ Máy chủ mặc định", - "LabelLastBookAdded": "Sách mới nhất được thêm", - "LabelLastBookUpdated": "Sách mới nhất được cập nhật", - "LabelLastSeen": "Lần cuối nhìn thấy", - "LabelLastTime": "Lần cuối", - "LabelLastUpdate": "Cập nhật cuối cùng", - "LabelLayout": "Bố cục", - "LabelLayoutSinglePage": "Một trang", - "LabelLayoutSplitPage": "Chia trang", - "LabelLess": "Ít hơn", - "LabelLibrariesAccessibleToUser": "Thư viện có thể truy cập cho người dùng", - "LabelLibrary": "Thư viện", - "LabelLibraryItem": "Mục thư viện", - "LabelLibraryName": "Tên thư viện", - "LabelLimit": "Giới hạn", - "LabelLineSpacing": "Khoảng cách dòng", - "LabelListenAgain": "Nghe lại", - "LabelLogLevelDebug": "Gỡ lỗi", - "LabelLogLevelInfo": "Thông tin", - "LabelLogLevelWarn": "Cảnh báo", - "LabelLookForNewEpisodesAfterDate": "Tìm tập mới sau ngày này", - "LabelLowestPriority": "Ưu tiên thấp nhất", - "LabelMatchExistingUsersBy": "Kết hợp người dùng hiện có theo", - "LabelMatchExistingUsersByDescription": "Sử dụng để kết nối người dùng hiện có. Khi kết nối, người dùng sẽ được kết hợp bằng một ID duy nhất từ nhà cung cấp SSO của bạn", - "LabelMediaPlayer": "Trình phát đa phương tiện", - "LabelMediaType": "Loại phương tiện", - "LabelMetadataOrderOfPrecedenceDescription": "Nguồn siêu dữ liệu ưu tiên cao hơn sẽ ghi đè lên các nguồn siêu dữ liệu ưu tiên thấp hơn", - "LabelMetadataProvider": "Nhà cung cấp siêu dữ liệu", - "LabelMetaTag": "Thẻ Meta", - "LabelMetaTags": "Các thẻ Meta", - "LabelMinute": "Phút", - "LabelMissing": "Thiếu", - "LabelMissingEbook": "Không có ebook", - "LabelMissingParts": "Các phần thiếu", - "LabelMissingSupplementaryEbook": "Không có ebook bổ sung", - "LabelMobileRedirectURIs": "URI chuyển hướng di động được cho phép", - "LabelMobileRedirectURIsDescription": "Đây là danh sách trắng các URI chuyển hướng hợp lệ cho ứng dụng di động. Mặc định là <code>audiobookshelf://oauth</code>, bạn có thể loại bỏ hoặc bổ sung thêm các URI cho tích hợp ứng dụng bên thứ ba. Sử dụng dấu hoa thị (<code>*</code>) như một mục duy nhất cho phép bất kỳ URI nào.", - "LabelMore": "Thêm", - "LabelMoreInfo": "Thông tin thêm", - "LabelName": "Tên", - "LabelNarrator": "Người kể", - "LabelNarrators": "Các người kể", - "LabelNew": "Mới", - "LabelNewestAuthors": "Nhà văn mới nhất", - "LabelNewestEpisodes": "Tập mới nhất", - "LabelNewPassword": "Mật khẩu mới", - "LabelNextBackupDate": "Ngày sao lưu tiếp theo", - "LabelNextScheduledRun": "Chạy tiếp theo theo lịch trình", - "LabelNoEpisodesSelected": "Không có tập nào được chọn", - "LabelNotes": "Ghi chú", - "LabelNotFinished": "Chưa hoàn thành", - "LabelNotificationAppriseURL": "URL(s) thông báo", - "LabelNotificationAvailableVariables": "Biến có sẵn", - "LabelNotificationBodyTemplate": "Mẫu Nội dung", - "LabelNotificationEvent": "Sự kiện Thông báo", - "LabelNotificationsMaxFailedAttempts": "Số lần thất bại tối đa", - "LabelNotificationsMaxFailedAttemptsHelp": "Thông báo sẽ bị vô hiệu hóa sau khi thất bại gửi số lần này", - "LabelNotificationsMaxQueueSize": "Kích thước hàng đợi tối đa cho sự kiện thông báo", - "LabelNotificationsMaxQueueSizeHelp": "Các sự kiện bị giới hạn mỗi giây chỉ gửi 1 lần. Các sự kiện sẽ bị bỏ qua nếu hàng đợi đạt kích thước tối đa. Điều này ngăn chặn spam thông báo.", - "LabelNotificationTitleTemplate": "Mẫu Tiêu đề", - "LabelNotStarted": "Chưa bắt đầu", - "LabelNumberOfBooks": "Số lượng Sách", - "LabelNumberOfEpisodes": "# của Tập", - "LabelOpenRSSFeed": "Mở RSS Feed", - "LabelOverwrite": "Ghi đè", - "LabelPassword": "Mật khẩu", - "LabelPath": "Đường dẫn", - "LabelPermissionsAccessAllLibraries": "Có Thể Truy Cập Tất Cả Thư Viện", - "LabelPermissionsAccessAllTags": "Có Thể Truy Cập Tất Cả Thẻ", - "LabelPermissionsAccessExplicitContent": "Có Thể Truy Cập Nội Dung Rõ Ràng", - "LabelPermissionsDelete": "Có Thể Xóa", - "LabelPermissionsDownload": "Có Thể Tải Xuống", - "LabelPermissionsUpdate": "Có Thể Cập Nhật", - "LabelPermissionsUpload": "Có Thể Tải Lên", - "LabelPersonalYearReview": "Năm của Bạn trong Bài Đánh Giá ({0})", - "LabelPhotoPathURL": "Đường dẫn/URL ảnh", - "LabelPlaylists": "Danh sách phát", - "LabelPlayMethod": "Phương pháp phát", - "LabelPodcast": "Podcast", - "LabelPodcasts": "Các podcast", - "LabelPodcastSearchRegion": "Vùng tìm kiếm podcast", - "LabelPodcastType": "Loại Podcast", - "LabelPort": "Cổng", - "LabelPrefixesToIgnore": "Tiền tố để bỏ qua (không phân biệt chữ hoa/chữ thường)", - "LabelPreventIndexing": "Ngăn chặn feed của bạn được chỉ mục bởi thư mục podcast của iTunes và Google", - "LabelPrimaryEbook": "Ebook chính", - "LabelProgress": "Tiến độ", - "LabelProvider": "Nhà cung cấp", - "LabelPubDate": "Ngày Xuất bản", - "LabelPublisher": "Nhà xuất bản", - "LabelPublishYear": "Năm Xuất bản", - "LabelRead": "Đọc", - "LabelReadAgain": "Đọc lại", - "LabelReadEbookWithoutProgress": "Đọc ebook mà không giữ tiến độ", - "LabelRecentlyAdded": "Gần đây thêm vào", - "LabelRecentSeries": "Loạt phim gần đây", - "LabelRecommended": "Được khuyến nghị", - "LabelRedo": "Làm lại", - "LabelRegion": "Khu vực", - "LabelReleaseDate": "Ngày Phát hành", - "LabelRemoveCover": "Xóa ảnh bìa", - "LabelRowsPerPage": "Số dòng mỗi trang", - "LabelRSSFeedCustomOwnerEmail": "Email chủ sở hữu tùy chỉnh", - "LabelRSSFeedCustomOwnerName": "Tên chủ sở hữu tùy chỉnh", - "LabelRSSFeedOpen": "Mở RSS Feed", - "LabelRSSFeedPreventIndexing": "Ngăn chặn Chỉ mục RSS Feed", - "LabelRSSFeedSlug": "Slug RSS Feed", - "LabelRSSFeedURL": "URL RSS Feed", - "LabelSearchTerm": "Thuật ngữ tìm kiếm", - "LabelSearchTitle": "Tìm kiếm Tiêu đề", - "LabelSearchTitleOrASIN": "Tìm kiếm Tiêu đề hoặc ASIN", - "LabelSeason": "Mùa", - "LabelSelectAllEpisodes": "Chọn tất cả các tập", - "LabelSelectEpisodesShowing": "Chọn {0} tập đang hiển thị", - "LabelSelectUsers": "Chọn người dùng", - "LabelSendEbookToDevice": "Gửi Ebook tới...", - "LabelSequence": "Trình tự", - "LabelSeries": "Loạt", - "LabelSeriesName": "Tên loạt", - "LabelSeriesProgress": "Tiến độ loạt", - "LabelServerYearReview": "Năm của Máy chủ trong Bài Đánh Giá ({0})", - "LabelSetEbookAsPrimary": "Đặt làm chính", - "LabelSetEbookAsSupplementary": "Đặt là bổ sung", - "LabelSettingsAudiobooksOnly": "Chỉ sách nói", - "LabelSettingsAudiobooksOnlyHelp": "Bật cài đặt này sẽ bỏ qua các tập tin ebook trừ khi chúng ở trong một thư mục sách nói, trong trường hợp đó chúng sẽ được đặt làm ebook bổ sung", - "LabelSettingsBookshelfViewHelp": "Thiết kế giả lập với kệ gỗ", - "LabelSettingsChromecastSupport": "Hỗ trợ Chromecast", - "LabelSettingsDateFormat": "Định dạng Ngày", - "LabelSettingsDisableWatcher": "Tắt Watcher", - "LabelSettingsDisableWatcherForLibrary": "Tắt watcher thư mục cho thư viện", - "LabelSettingsDisableWatcherHelp": "Tắt chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ", - "LabelSettingsEnableWatcher": "Bật Watcher", - "LabelSettingsEnableWatcherForLibrary": "Bật watcher thư mục cho thư viện", - "LabelSettingsEnableWatcherHelp": "Bật chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ", - "LabelSettingsExperimentalFeatures": "Tính năng thử nghiệm", - "LabelSettingsExperimentalFeaturesHelp": "Các tính năng đang phát triển có thể cần phản hồi của bạn và sự giúp đỡ trong thử nghiệm. Nhấp để mở thảo luận trên github.", - "LabelSettingsFindCovers": "Tìm ảnh bìa", - "LabelSettingsFindCoversHelp": "Nếu sách nói của bạn không có ảnh bìa nhúng hoặc ảnh bìa trong thư mục, trình quét sẽ cố gắng tìm ảnh bìa.<br>Lưu ý: Điều này sẽ kéo dài thời gian quét", - "LabelSettingsHideSingleBookSeries": "Ẩn loạt sách đơn lẻ", - "LabelSettingsHideSingleBookSeriesHelp": "Các loạt sách chỉ có một cuốn sách sẽ được ẩn khỏi trang loạt sách và kệ trang chủ.", - "LabelSettingsHomePageBookshelfView": "Trang chủ sử dụng chế độ xem kệ sách", - "LabelSettingsLibraryBookshelfView": "Thư viện sử dụng chế độ xem kệ sách", - "LabelSettingsParseSubtitles": "Phân tích phụ đề", - "LabelSettingsParseSubtitlesHelp": "Trích xuất phụ đề từ tên thư mục sách nói.<br>Phụ đề phải được tách bằng \" - \"<br>i.e. \"Book Title - A Subtitle Here\" có phụ đề \"A Subtitle Here\"", - "LabelSettingsPreferMatchedMetadata": "Ưu tiên siêu dữ liệu phù hợp", - "LabelSettingsPreferMatchedMetadataHelp": "Dữ liệu phù hợp sẽ ghi đè lên chi tiết mục khi sử dụng Kết hợp Nhanh. Theo mặc định, Kết hợp Nhanh chỉ điền vào các chi tiết bị thiếu.", - "LabelSettingsSkipMatchingBooksWithASIN": "Bỏ qua sách khớp có ASIN", - "LabelSettingsSkipMatchingBooksWithISBN": "Bỏ qua sách khớp có ISBN", - "LabelSettingsSortingIgnorePrefixes": "Bỏ qua tiền tố khi sắp xếp", - "LabelSettingsSortingIgnorePrefixesHelp": "ví dụ. với tiền tố \"the\" tiêu đề sách \"The Book Title\" sẽ được sắp xếp như \"Book Title, The\"", - "LabelSettingsSquareBookCovers": "Sử dụng ảnh bìa vuông", - "LabelSettingsSquareBookCoversHelp": "Ưu tiên sử dụng ảnh bìa vuông hơn ảnh bìa tiêu chuẩn 1.6:1", - "LabelSettingsStoreCoversWithItem": "Lưu trữ ảnh bìa với mục", - "LabelSettingsStoreCoversWithItemHelp": "Theo mặc định, ảnh bìa được lưu trữ trong /metadata/items, bật cài đặt này sẽ lưu trữ ảnh bìa trong thư mục mục của thư viện bạn. Chỉ một tệp có tên là \"cover\" sẽ được giữ lại", - "LabelSettingsStoreMetadataWithItem": "Lưu trữ siêu dữ liệu với mục", - "LabelSettingsStoreMetadataWithItemHelp": "Theo mặc định, các tệp siêu dữ liệu được lưu trữ trong /metadata/items, bật cài đặt này sẽ lưu trữ các tệp siêu dữ liệu trong các thư mục mục của thư viện bạn", - "LabelSettingsTimeFormat": "Định dạng Thời gian", - "LabelShowAll": "Hiển thị Tất cả", - "LabelSize": "Kích thước", - "LabelSleepTimer": "Hẹn giờ tắt", - "LabelSlug": "Slug", - "LabelStart": "Bắt đầu", - "LabelStarted": "Đã bắt đầu", - "LabelStartedAt": "Bắt đầu vào", - "LabelStartTime": "Thời gian bắt đầu", - "LabelStatsAudioTracks": "Audio Tracks", - "LabelStatsAuthors": "Tác giả", - "LabelStatsBestDay": "Ngày tốt nhất", - "LabelStatsDailyAverage": "Trung bình hàng ngày", - "LabelStatsDays": "Ngày", - "LabelStatsDaysListened": "Ngày đã nghe", - "LabelStatsHours": "Giờ", - "LabelStatsInARow": "liên tiếp", - "LabelStatsItemsFinished": "Mục đã hoàn thành", - "LabelStatsItemsInLibrary": "Mục trong thư viện", - "LabelStatsMinutes": "phút", - "LabelStatsMinutesListening": "Phút Nghe", - "LabelStatsOverallDays": "Tổng số ngày", - "LabelStatsOverallHours": "Tổng số giờ", - "LabelStatsWeekListening": "Tuần nghe", - "LabelSubtitle": "Phụ đề", - "LabelSupportedFileTypes": "Loại tệp được hỗ trợ", - "LabelTag": "Thẻ", - "LabelTags": "Thẻ", - "LabelTagsAccessibleToUser": "Thẻ Có Thể Truy Cập Cho Người Dùng", - "LabelTagsNotAccessibleToUser": "Thẻ Không Thể Truy Cập Cho Người Dùng", - "LabelTasks": "Nhiệm vụ Đang chạy", - "LabelTextEditorBulletedList": "Danh sách có dấu đầu dòng", - "LabelTextEditorLink": "Liên kết", - "LabelTextEditorNumberedList": "Danh sách đánh số", - "LabelTextEditorUnlink": "Gỡ liên kết", - "LabelTheme": "Chủ đề", - "LabelThemeDark": "Tối", - "LabelThemeLight": "Sáng", - "LabelTimeBase": "Thời gian cơ bản", - "LabelTimeListened": "Thời gian đã nghe", - "LabelTimeListenedToday": "Thời gian đã nghe hôm nay", - "LabelTimeRemaining": "{0} còn lại", - "LabelTimeToShift": "Thời gian dời chuyển theo giây", - "LabelTitle": "Tiêu đề", - "LabelToolsEmbedMetadata": "Nhúng siêu dữ liệu", - "LabelToolsEmbedMetadataDescription": "Nhúng siêu dữ liệu vào tệp âm thanh bao gồm ảnh bìa và chương.", - "LabelToolsMakeM4b": "Tạo Tệp Audiobook M4B", - "LabelToolsMakeM4bDescription": "Tạo tệp audiobook .M4B với siêu dữ liệu nhúng, ảnh bìa và chương.", - "LabelToolsSplitM4b": "Chia M4B thành MP3", - "LabelToolsSplitM4bDescription": "Tạo MP3 từ M4B được chia theo chương với siêu dữ liệu nhúng, ảnh bìa và chương.", - "LabelTotalDuration": "Tổng thời lượng", - "LabelTotalTimeListened": "Tổng thời gian đã nghe", - "LabelTrackFromFilename": "Từ tên tệp", - "LabelTrackFromMetadata": "Từ siêu dữ liệu", - "LabelTracks": "Bài hát", - "LabelTracksMultiTrack": "Nhiều track", - "LabelTracksNone": "Không có track", - "LabelTracksSingleTrack": "Một track", - "LabelType": "Loại", - "LabelUnabridged": "Không rút gọn", - "LabelUndo": "Hoàn tác", - "LabelUnknown": "Không xác định", - "LabelUpdateCover": "Cập nhật ảnh bìa", - "LabelUpdateCoverHelp": "Cho phép ghi đè lên các ảnh bìa hiện có cho các cuốn sách được chọn khi tìm thấy một kết hợp", - "LabelUpdatedAt": "Cập nhật lúc", - "LabelUpdateDetails": "Cập nhật chi tiết", - "LabelUpdateDetailsHelp": "Cho phép ghi đè lên các chi tiết hiện có cho các cuốn sách được chọn khi tìm thấy một kết hợp", - "LabelUploaderDragAndDrop": "Kéo và thả tệp hoặc thư mục", - "LabelUploaderDropFiles": "Thả tệp", - "LabelUploaderItemFetchMetadataHelp": "Tự động lấy tiêu đề, tác giả và loạt", - "LabelUseChapterTrack": "Sử dụng track chương", - "LabelUseFullTrack": "Sử dụng toàn bộ track", - "LabelUser": "Người dùng", - "LabelUsername": "Tên người dùng", - "LabelValue": "Giá trị", - "LabelVersion": "Phiên bản", - "LabelViewBookmarks": "Xem các đánh dấu", - "LabelViewChapters": "Xem các chương", - "LabelViewQueue": "Xem hàng đợi phát", - "LabelVolume": "Âm lượng", - "LabelWeekdaysToRun": "Ngày trong tuần để chạy", - "LabelYearReviewHide": "Ẩn Năm trong Bài Đánh Giá", - "LabelYearReviewShow": "Xem Năm trong Bài Đánh Giá", - "LabelYourAudiobookDuration": "Thời lượng sách nói của bạn", - "LabelYourBookmarks": "Đánh dấu của bạn", - "LabelYourPlaylists": "Danh sách phát của bạn", - "LabelYourProgress": "Tiến trình của bạn", - "MessageAddToPlayerQueue": "Thêm vào hàng đợi phát", - "MessageAppriseDescription": "Để sử dụng tính năng này, bạn cần có một phiên bản của <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> đang chạy hoặc một api sẽ xử lý các yêu cầu tương tự. <br /> Địa chỉ URL của Apprise API nên là đường dẫn URL đầy đủ để gửi thông báo, ví dụ, nếu phiên bản API của bạn được phục vụ tại <code>http://192.168.1.1:8337</code> thì bạn sẽ đặt <code>http://192.168.1.1:8337/notify</code>.", - "MessageBackupsDescription": "Bản sao bao gồm người dùng, tiến độ của người dùng, chi tiết mục thư viện, cài đặt máy chủ và hình ảnh được lưu trữ trong <code>/metadata/items</code> & <code>/metadata/authors</code>. Bản sao <strong>không</strong> bao gồm bất kỳ tệp nào được lưu trữ trong các thư mục thư viện của bạn.", - "MessageBatchQuickMatchDescription": "Quick Match sẽ cố gắng thêm các ảnh bìa và siêu dữ liệu bị thiếu cho các mục đã chọn. Bật các tùy chọn dưới đây để cho phép Quick Match ghi đè lên các ảnh bìa hiện có và / hoặc siêu dữ liệu.", - "MessageBookshelfNoCollections": "Bạn chưa tạo bất kỳ bộ sưu tập nào", - "MessageBookshelfNoResultsForFilter": "Không có Kết quả cho bộ lọc \"{0}: {1}\"", - "MessageBookshelfNoRSSFeeds": "Không có nguồn cung cấp RSS nào đang mở", - "MessageBookshelfNoSeries": "Bạn không có bộ sách", - "MessageChapterEndIsAfter": "Kết thúc chương sau khi kết thúc sách nói của bạn", - "MessageChapterErrorFirstNotZero": "Chương đầu tiên phải bắt đầu từ 0", - "MessageChapterErrorStartGteDuration": "Thời gian bắt đầu không hợp lệ phải nhỏ hơn thời lượng sách nói", - "MessageChapterErrorStartLtPrev": "Thời gian bắt đầu không hợp lệ phải lớn hơn hoặc bằng thời gian bắt đầu của chương trước", - "MessageChapterStartIsAfter": "Bắt đầu chương sau khi kết thúc sách nói của bạn", - "MessageCheckingCron": "Kiểm tra cron...", - "MessageConfirmCloseFeed": "Bạn có chắc chắn muốn đóng nguồn cung cấp này không?", - "MessageConfirmDeleteBackup": "Bạn có chắc chắn muốn xóa bản sao lưu cho {0} không?", - "MessageConfirmDeleteFile": "Điều này sẽ xóa tệp khỏi hệ thống tệp của bạn. Bạn có chắc chắn không?", - "MessageConfirmDeleteLibrary": "Bạn có chắc chắn muốn xóa vĩnh viễn thư viện \"{0}\" không?", - "MessageConfirmDeleteLibraryItem": "Điều này sẽ xóa mục thư viện khỏi cơ sở dữ liệu và hệ thống tệp của bạn. Bạn có chắc chắn không?", - "MessageConfirmDeleteLibraryItems": "Điều này sẽ xóa {0} mục thư viện khỏi cơ sở dữ liệu và hệ thống tệp của bạn. Bạn có chắc chắn không?", - "MessageConfirmDeleteSession": "Bạn có chắc chắn muốn xóa phiên này không?", - "MessageConfirmForceReScan": "Bạn có chắc chắn muốn buộc quét lại không?", - "MessageConfirmMarkAllEpisodesFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các tập phim đã kết thúc không?", - "MessageConfirmMarkAllEpisodesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các tập phim chưa kết thúc không?", - "MessageConfirmMarkSeriesFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này đã kết thúc không?", - "MessageConfirmMarkSeriesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này chưa kết thúc không?", - "MessageConfirmQuickEmbed": "Cảnh báo! Quick embed sẽ không sao lưu các tệp âm thanh của bạn. Đảm bảo bạn có một bản sao lưu của các tệp âm thanh của bạn. <br><br>Bạn có muốn tiếp tục không?", - "MessageConfirmRemoveAllChapters": "Bạn có chắc chắn muốn xóa tất cả các chương không?", - "MessageConfirmRemoveAuthor": "Bạn có chắc chắn muốn xóa tác giả \"{0}\" không?", - "MessageConfirmRemoveCollection": "Bạn có chắc chắn muốn xóa bộ sưu tập \"{0}\" không?", - "MessageConfirmRemoveEpisode": "Bạn có chắc chắn muốn xóa tập phim \"{0}\" không?", - "MessageConfirmRemoveEpisodes": "Bạn có chắc chắn muốn xóa {0} tập phim không?", - "MessageConfirmRemoveListeningSessions": "Bạn có chắc chắn muốn xóa {0} phiên nghe không?", - "MessageConfirmRemoveNarrator": "Bạn có chắc chắn muốn xóa người kể chuyện \"{0}\" không?", - "MessageConfirmRemovePlaylist": "Bạn có chắc chắn muốn xóa danh sách phát của bạn \"{0}\" không?", - "MessageConfirmRenameGenre": "Bạn có chắc chắn muốn đổi tên thể loại \"{0}\" thành \"{1}\" cho tất cả các mục không?", - "MessageConfirmRenameGenreMergeNote": "Lưu ý: Thể loại này đã tồn tại nên chúng sẽ được hợp nhất.", - "MessageConfirmRenameGenreWarning": "Cảnh báo! Một thể loại tương tự với kiểu chữ khác đã tồn tại \"{0}\".", - "MessageConfirmRenameTag": "Bạn có chắc chắn muốn đổi tên tag \"{0}\" thành \"{1}\" cho tất cả các mục không?", - "MessageConfirmRenameTagMergeNote": "Lưu ý: Thẻ này đã tồn tại nên chúng sẽ được hợp nhất.", - "MessageConfirmRenameTagWarning": "Cảnh báo! Một thẻ tương tự với kiểu chữ khác đã tồn tại \"{0}\".", - "MessageConfirmReScanLibraryItems": "Bạn có chắc chắn muốn quét lại {0} mục không?", - "MessageConfirmSendEbookToDevice": "Bạn có chắc chắn muốn gửi {0} ebook \"{1}\" đến thiết bị \"{2}\" không?", - "MessageDownloadingEpisode": "Đang tải tập phim", - "MessageDragFilesIntoTrackOrder": "Kéo tệp vào thứ tự track đúng", - "MessageEmbedFinished": "Nhúng Hoàn thành!", - "MessageEpisodesQueuedForDownload": "{0} Tập(s) đã được thêm vào hàng đợi để tải xuống", - "MessageFeedURLWillBe": "URL nguồn cấp sẽ là {0}", - "MessageFetching": "Đang tìm...", - "MessageForceReScanDescription": "sẽ quét lại tất cả các tệp như một quét mới. Các thẻ ID3 của tệp âm thanh, tệp OPF và tệp văn bản sẽ được quét làm mới.", - "MessageImportantNotice": "Thông báo quan trọng!", - "MessageInsertChapterBelow": "Chèn chương dưới đây", - "MessageItemsSelected": "{0} Mục Đã Chọn", - "MessageItemsUpdated": "{0} Mục Đã Cập Nhật", - "MessageJoinUsOn": "Tham gia cùng chúng tôi trên", - "MessageListeningSessionsInTheLastYear": "{0} phiên nghe trong năm qua", - "MessageLoading": "Đang tải...", - "MessageLoadingFolders": "Đang tải các thư mục...", - "MessageM4BFailed": "M4B thất bại!", - "MessageM4BFinished": "M4B Hoàn thành!", - "MessageMapChapterTitles": "Ánh xạ tiêu đề chương với các chương hiện có của sách audio của bạn mà không điều chỉnh thời gian", - "MessageMarkAllEpisodesFinished": "Đánh dấu tất cả các tập phim đã kết thúc", - "MessageMarkAllEpisodesNotFinished": "Đánh dấu tất cả các tập phim chưa kết thúc", - "MessageMarkAsFinished": "Đánh dấu là Đã Kết Thúc", - "MessageMarkAsNotFinished": "Đánh dấu là Chưa Kết Thúc", - "MessageMatchBooksDescription": "sẽ cố gắng kết hợp các sách trong thư viện với một cuốn sách từ nhà cung cấp tìm kiếm được chọn và điền vào các chi tiết trống và ảnh bìa. Không ghi đè các chi tiết.", - "MessageNoAudioTracks": "Không có track âm thanh", - "MessageNoAuthors": "Không có Tác giả", - "MessageNoBackups": "Không có Bản sao lưu", - "MessageNoBookmarks": "Không có Đánh dấu", - "MessageNoChapters": "Không có Chương", - "MessageNoCollections": "Không có Bộ sưu tập", - "MessageNoCoversFound": "Không tìm thấy Ảnh bìa", - "MessageNoDescription": "Không có mô tả", - "MessageNoDownloadsInProgress": "Không có tải xuống đang tiến hành", - "MessageNoDownloadsQueued": "Không có tải xuống được xếp hàng", - "MessageNoEpisodeMatchesFound": "Không tìm thấy tập phim nào phù hợp", - "MessageNoEpisodes": "Không có Tập phim", - "MessageNoFoldersAvailable": "Không có Thư mục nào có sẵn", - "MessageNoGenres": "Không có Thể loại", - "MessageNoIssues": "Không có Vấn đề", - "MessageNoItems": "Không có Mục", - "MessageNoItemsFound": "Không tìm thấy mục nào", - "MessageNoListeningSessions": "Không có Phiên Nghe", - "MessageNoLogs": "Không có Log", - "MessageNoMediaProgress": "Không có Tiến độ Phương tiện", - "MessageNoNotifications": "Không có Thông báo", - "MessageNoPodcastsFound": "Không tìm thấy podcast nào", - "MessageNoResults": "Không có Kết quả", - "MessageNoSearchResultsFor": "Không có kết quả tìm kiếm cho \"{0}\"", - "MessageNoSeries": "Không có Bộ", - "MessageNoTags": "Không có Thẻ", - "MessageNoTasksRunning": "Không có Công việc đang chạy", - "MessageNotYetImplemented": "Chưa được triển khai", - "MessageNoUpdateNecessary": "Không cần cập nhật", - "MessageNoUpdatesWereNecessary": "Không cần cập nhật", - "MessageNoUserPlaylists": "Bạn chưa có danh sách phát", - "MessageOr": "hoặc", - "MessagePauseChapter": "Tạm dừng phát chương", - "MessagePlayChapter": "Nghe từ đầu chương", - "MessagePlaylistCreateFromCollection": "Tạo danh sách phát từ bộ sưu tập", - "MessagePodcastHasNoRSSFeedForMatching": "Podcast không có RSS feed để sử dụng cho việc kết hợp", - "MessageQuickMatchDescription": "Điền chi tiết mục trống và ảnh bìa với kết quả phù hợp đầu tiên từ '{0}'. Không ghi đè chi tiết trừ khi cài đặt máy chủ 'Ưu tiên dữ liệu phù hợp' được bật.", - "MessageRemoveChapter": "Xóa chương", - "MessageRemoveEpisodes": "Xóa {0} tập", - "MessageRemoveFromPlayerQueue": "Xóa khỏi hàng đợi phát", - "MessageRemoveUserWarning": "Bạn có chắc chắn muốn xóa người dùng \"{0}\" một cách vĩnh viễn không?", - "MessageReportBugsAndContribute": "Báo cáo lỗi, yêu cầu tính năng và đóng góp tại", - "MessageResetChaptersConfirm": "Bạn có chắc chắn muốn đặt lại các chương và hủy những thay đổi bạn đã thực hiện không?", - "MessageRestoreBackupConfirm": "Bạn có chắc chắn muốn khôi phục bản sao lưu được tạo vào", - "MessageRestoreBackupWarning": "Việc khôi phục bản sao lưu sẽ ghi đè lên toàn bộ cơ sở dữ liệu được đặt tại /config và ảnh bìa trong /metadata/items & /metadata/authors.<br /><br />Bản sao lưu không sửa đổi bất kỳ tệp nào trong các thư mục thư viện của bạn. Nếu bạn đã bật các cài đặt máy chủ để lưu ảnh bìa và dữ liệu phần mềm trong các thư mục thư viện của mình thì chúng sẽ không được sao lưu hoặc ghi đè.<br /><br />Tất cả các máy khách sử dụng máy chủ của bạn sẽ được làm mới tự động.", - "MessageSearchResultsFor": "Kết quả tìm kiếm cho", - "MessageSelected": "{0} đã được chọn", - "MessageServerCouldNotBeReached": "Không thể kết nối đến máy chủ", - "MessageSetChaptersFromTracksDescription": "Đặt chương sử dụng mỗi tệp âm thanh là một chương và tiêu đề chương là tên tệp âm thanh", - "MessageStartPlaybackAtTime": "Bắt đầu phát \"{0}\" tại thời điểm {1}?", - "MessageThinking": "Đang suy nghĩ...", - "MessageUploaderItemFailed": "Không thể tải lên", - "MessageUploaderItemSuccess": "Tải lên thành công!", - "MessageUploading": "Đang tải lên...", - "MessageValidCronExpression": "Biểu thức cron hợp lệ", - "MessageWatcherIsDisabledGlobally": "Watcher đã bị vô hiệu hóa toàn cầu trong cài đặt máy chủ", - "MessageXLibraryIsEmpty": "Thư viện {0} rỗng!", - "MessageYourAudiobookDurationIsLonger": "Thời lượng sách nói của bạn dài hơn so với thời lượng tìm thấy", - "MessageYourAudiobookDurationIsShorter": "Thời lượng sách nói của bạn ngắn hơn so với thời lượng tìm thấy", - "NoteChangeRootPassword": "Người dùng Root là người dùng duy nhất có thể có mật khẩu trống", - "NoteChapterEditorTimes": "Lưu ý: Thời gian bắt đầu của chương đầu tiên phải ở 0:00 và thời gian bắt đầu của chương cuối cùng không thể vượt quá thời lượng của sách nói này.", - "NoteFolderPicker": "Lưu ý: các thư mục đã được ánh xạ trước đó sẽ không được hiển thị", - "NoteRSSFeedPodcastAppsHttps": "Cảnh báo: Hầu hết các ứng dụng podcast sẽ yêu cầu URL của RSS feed sử dụng HTTPS", - "NoteRSSFeedPodcastAppsPubDate": "Cảnh báo: 1 hoặc nhiều tập của bạn không có Pub Date. Một số ứng dụng podcast yêu cầu điều này.", - "NoteUploaderFoldersWithMediaFiles": "Các thư mục có tệp phương tiện sẽ được xử lý như các mục thư viện riêng biệt.", - "NoteUploaderOnlyAudioFiles": "Nếu chỉ tải lên các tệp âm thanh thì mỗi tệp âm thanh sẽ được xử lý như một cuốn sách nói riêng biệt.", - "NoteUploaderUnsupportedFiles": "Các tệp không được hỗ trợ sẽ bị bỏ qua. Khi chọn hoặc thả một thư mục, các tệp khác không có trong thư mục mục sẽ bị bỏ qua.", - "PlaceholderNewCollection": "Tên bộ sưu tập mới", - "PlaceholderNewFolderPath": "Đường dẫn thư mục mới", - "PlaceholderNewPlaylist": "Tên danh sách phát mới", - "PlaceholderSearch": "Tìm kiếm..", - "PlaceholderSearchEpisode": "Tìm kiếm tập..", - "ToastAccountUpdateFailed": "Cập nhật tài khoản thất bại", - "ToastAccountUpdateSuccess": "Tài khoản đã được cập nhật", - "ToastAuthorImageRemoveFailed": "Không thể xóa ảnh tác giả", - "ToastAuthorImageRemoveSuccess": "Ảnh tác giả đã được xóa", - "ToastAuthorUpdateFailed": "Cập nhật tác giả thất bại", - "ToastAuthorUpdateMerged": "Tác giả đã được hợp nhất", - "ToastAuthorUpdateSuccess": "Cập nhật tác giả thành công", - "ToastAuthorUpdateSuccessNoImageFound": "Cập nhật tác giả thành công (không tìm thấy ảnh)", - "ToastBackupCreateFailed": "Tạo bản sao lưu thất bại", - "ToastBackupCreateSuccess": "Bản sao lưu được tạo", - "ToastBackupDeleteFailed": "Xóa bản sao lưu thất bại", - "ToastBackupDeleteSuccess": "Bản sao lưu đã được xóa", - "ToastBackupRestoreFailed": "Khôi phục bản sao lưu thất bại", - "ToastBackupUploadFailed": "Tải lên bản sao lưu thất bại", - "ToastBackupUploadSuccess": "Bản sao lưu đã được tải lên", - "ToastBatchUpdateFailed": "Cập nhật nhóm thất bại", - "ToastBatchUpdateSuccess": "Cập nhật nhóm thành công", - "ToastBookmarkCreateFailed": "Tạo đánh dấu thất bại", - "ToastBookmarkCreateSuccess": "Đã thêm đánh dấu", - "ToastBookmarkRemoveFailed": "Xóa đánh dấu thất bại", - "ToastBookmarkRemoveSuccess": "Đánh dấu đã được xóa", - "ToastBookmarkUpdateFailed": "Cập nhật đánh dấu thất bại", - "ToastBookmarkUpdateSuccess": "Đánh dấu đã được cập nhật", - "ToastChaptersHaveErrors": "Các chương có lỗi", - "ToastChaptersMustHaveTitles": "Các chương phải có tiêu đề", - "ToastCollectionItemsRemoveFailed": "Xóa mục từ bộ sưu tập thất bại", - "ToastCollectionItemsRemoveSuccess": "Mục đã được xóa khỏi bộ sưu tập", - "ToastCollectionRemoveFailed": "Xóa bộ sưu tập thất bại", - "ToastCollectionRemoveSuccess": "Bộ sưu tập đã được xóa", - "ToastCollectionUpdateFailed": "Cập nhật bộ sưu tập thất bại", - "ToastCollectionUpdateSuccess": "Bộ sưu tập đã được cập nhật", - "ToastItemCoverUpdateFailed": "Cập nhật ảnh bìa mục thất bại", - "ToastItemCoverUpdateSuccess": "Ảnh bìa mục đã được cập nhật", - "ToastItemDetailsUpdateFailed": "Cập nhật chi tiết mục thất bại", - "ToastItemDetailsUpdateSuccess": "Chi tiết mục đã được cập nhật", - "ToastItemDetailsUpdateUnneeded": "Không cần cập nhật chi tiết mục", - "ToastItemMarkedAsFinishedFailed": "Đánh dấu mục là Hoàn thành thất bại", - "ToastItemMarkedAsFinishedSuccess": "Mục đã được đánh dấu là Hoàn thành", - "ToastItemMarkedAsNotFinishedFailed": "Đánh dấu mục là Chưa hoàn thành thất bại", - "ToastItemMarkedAsNotFinishedSuccess": "Mục đã được đánh dấu là Chưa hoàn thành", - "ToastLibraryCreateFailed": "Tạo thư viện thất bại", - "ToastLibraryCreateSuccess": "Thư viện \"{0}\" đã được tạo", - "ToastLibraryDeleteFailed": "Xóa thư viện thất bại", - "ToastLibraryDeleteSuccess": "Thư viện đã được xóa", - "ToastLibraryScanFailedToStart": "Không thể bắt đầu quét thư viện", - "ToastLibraryScanStarted": "Quét thư viện đã được bắt đầu", - "ToastLibraryUpdateFailed": "Cập nhật thư viện thất bại", - "ToastLibraryUpdateSuccess": "Thư viện \"{0}\" đã được cập nhật", - "ToastPlaylistCreateFailed": "Tạo danh sách phát thất bại", - "ToastPlaylistCreateSuccess": "Danh sách phát đã được tạo", - "ToastPlaylistRemoveFailed": "Xóa danh sách phát thất bại", - "ToastPlaylistRemoveSuccess": "Danh sách phát đã được xóa", - "ToastPlaylistUpdateFailed": "Cập nhật danh sách phát thất bại", - "ToastPlaylistUpdateSuccess": "Danh sách phát đã được cập nhật", - "ToastPodcastCreateFailed": "Tạo podcast thất bại", - "ToastPodcastCreateSuccess": "Podcast đã được tạo thành công", - "ToastRemoveItemFromCollectionFailed": "Xóa mục khỏi bộ sưu tập thất bại", - "ToastRemoveItemFromCollectionSuccess": "Mục đã được xóa khỏi bộ sưu tập", - "ToastRSSFeedCloseFailed": "Đóng nguồn cấp RSS thất bại", - "ToastRSSFeedCloseSuccess": "Nguồn cấp RSS đã được đóng", - "ToastSendEbookToDeviceFailed": "Gửi ebook đến thiết bị thất bại", - "ToastSendEbookToDeviceSuccess": "Ebook đã được gửi đến thiết bị \"{0}\"", - "ToastSeriesUpdateFailed": "Cập nhật loạt truyện thất bại", - "ToastSeriesUpdateSuccess": "Cập nhật loạt truyện thành công", - "ToastSessionDeleteFailed": "Xóa phiên thất bại", - "ToastSessionDeleteSuccess": "Phiên đã được xóa", - "ToastSocketConnected": "Kết nối socket", - "ToastSocketDisconnected": "Ngắt kết nối socket", - "ToastSocketFailedToConnect": "Không thể kết nối socket", - "ToastUserDeleteFailed": "Xóa người dùng thất bại", - "ToastUserDeleteSuccess": "Người dùng đã được xóa" -} \ No newline at end of file + "ButtonAdd": "Thêm", + "ButtonAddChapters": "Thêm Chương", + "ButtonAddDevice": "Thêm Thiết Bị", + "ButtonAddLibrary": "Thêm Thư Viện", + "ButtonAddPodcasts": "Thêm Podcasts", + "ButtonAddUser": "Thêm Người Dùng", + "ButtonAddYourFirstLibrary": "Thêm thư viện đầu tiên của bạn", + "ButtonApply": "Áp Dụng", + "ButtonApplyChapters": "Áp Dụng Chương", + "ButtonAuthors": "Tác Giả", + "ButtonBrowseForFolder": "Duyệt Thư Mục", + "ButtonCancel": "Hủy", + "ButtonCancelEncode": "Hủy Mã Hóa", + "ButtonChangeRootPassword": "Thay Đổi Mật Khẩu Root", + "ButtonCheckAndDownloadNewEpisodes": "Kiểm Tra và Tải Xuống Các Tập Phim Mới", + "ButtonChooseAFolder": "Chọn một thư mục", + "ButtonChooseFiles": "Chọn tập tin", + "ButtonClearFilter": "Xóa Bộ Lọc", + "ButtonCloseFeed": "Đóng Feed", + "ButtonCollections": "Bộ Sưu Tập", + "ButtonConfigureScanner": "Cấu Hình Bộ Quét", + "ButtonCreate": "Tạo", + "ButtonCreateBackup": "Tạo Bản Sao Lưu", + "ButtonDelete": "Xóa", + "ButtonDownloadQueue": "Hàng Chờ", + "ButtonEdit": "Chỉnh Sửa", + "ButtonEditChapters": "Chỉnh Sửa Chương", + "ButtonEditPodcast": "Chỉnh Sửa Podcast", + "ButtonForceReScan": "Force Re-Scan", + "ButtonFullPath": "Đường Dẫn Đầy Đủ", + "ButtonHide": "Ẩn", + "ButtonHome": "Trang Chủ", + "ButtonIssues": "Vấn Đề", + "ButtonJumpBackward": "Bước Lùi", + "ButtonJumpForward": "Bước Tiến", + "ButtonLatest": "Mới Nhất", + "ButtonLibrary": "Thư Viện", + "ButtonLogout": "Đăng Xuất", + "ButtonLookup": "Tra Cứu", + "ButtonManageTracks": "Quản Lý Tracks", + "ButtonMapChapterTitles": "Ánh Xạ Tiêu Đề Chương", + "ButtonMatchAllAuthors": "Khớp Tất Cả Tác Giả", + "ButtonMatchBooks": "Khớp Sách", + "ButtonNevermind": "Không Sao", + "ButtonNext": "Tiếp Theo", + "ButtonNextChapter": "Chương Tiếp Theo", + "ButtonOk": "Ok", + "ButtonOpenFeed": "Mở Feed", + "ButtonOpenManager": "Mở Quản Lý", + "ButtonPause": "Tạm Dừng", + "ButtonPlay": "Phát", + "ButtonPlaying": "Đang Phát", + "ButtonPlaylists": "Danh Sách Phát", + "ButtonPrevious": "Trước", + "ButtonPreviousChapter": "Chương Trước", + "ButtonPurgeAllCache": "Xóa Sạch Tất Cả Bộ Nhớ Cache", + "ButtonPurgeItemsCache": "Xóa Sạch Bộ Nhớ Cache Các Mục", + "ButtonPurgeMediaProgress": "Xóa Sạch Tiến Trình Phương Tiện", + "ButtonQueueAddItem": "Thêm vào hàng đợi", + "ButtonQueueRemoveItem": "Xóa khỏi hàng đợi", + "ButtonQuickMatch": "Khớp Nhanh", + "ButtonRead": "Đọc", + "ButtonRefresh": "Làm Mới", + "ButtonRemove": "Xóa", + "ButtonRemoveAll": "Xóa Tất Cả", + "ButtonRemoveAllLibraryItems": "Xóa Tất Cả Các Mục Thư Viện", + "ButtonRemoveFromContinueListening": "Xóa khỏi Tiếp Tục Nghe", + "ButtonRemoveFromContinueReading": "Xóa khỏi Tiếp Tục Đọc", + "ButtonRemoveSeriesFromContinueSeries": "Xóa Series khỏi Tiếp Tục Series", + "ButtonReScan": "Quét Lại", + "ButtonReset": "Đặt Lại", + "ButtonResetToDefault": "Đặt Lại về Mặc Định", + "ButtonRestore": "Khôi Phục", + "ButtonSave": "Lưu", + "ButtonSaveAndClose": "Lưu & Đóng", + "ButtonSaveTracklist": "Lưu Danh Sách Track", + "ButtonScan": "Quét", + "ButtonScanLibrary": "Quét Thư Viện", + "ButtonSearch": "Tìm Kiếm", + "ButtonSelectFolderPath": "Chọn Đường Dẫn Thư Mục", + "ButtonSeries": "Series", + "ButtonSetChaptersFromTracks": "Đặt chương từ các track", + "ButtonShare": "Chia Sẻ", + "ButtonShiftTimes": "Dời Thời Gian", + "ButtonShow": "Hiện", + "ButtonStartM4BEncode": "Bắt đầu Mã Hóa M4B", + "ButtonStartMetadataEmbed": "Bắt đầu Nhúng Dữ Liệu", + "ButtonSubmit": "Gửi", + "ButtonTest": "Kiểm Tra", + "ButtonUpload": "Tải Lên", + "ButtonUploadBackup": "Tải Lên Bản Sao Lưu", + "ButtonUploadCover": "Tải Lên Bìa", + "ButtonUploadOPMLFile": "Tải Lên Tệp OPML", + "ButtonUserDelete": "Xóa người dùng {0}", + "ButtonUserEdit": "Chỉnh Sửa người dùng {0}", + "ButtonViewAll": "Xem Tất Cả", + "ButtonYes": "Có", + "ErrorUploadFetchMetadataAPI": "Lỗi khi lấy dữ liệu metadata", + "ErrorUploadFetchMetadataNoResults": "Không thể lấy dữ liệu metadata - hãy thử cập nhật tiêu đề và/hoặc tác giả", + "ErrorUploadLacksTitle": "Phải có một tiêu đề", + "HeaderAccount": "Tài Khoản", + "HeaderAdvanced": "Nâng Cao", + "HeaderAppriseNotificationSettings": "Cài Đặt Thông Báo Apprise", + "HeaderAudiobookTools": "Công Cụ Quản Lý Tệp Truyện Nói", + "HeaderAudioTracks": "Các Track Âm Thanh", + "HeaderAuthentication": "Xác Thực", + "HeaderBackups": "Bản Sao Lưu", + "HeaderChangePassword": "Thay Đổi Mật Khẩu", + "HeaderChapters": "Chương", + "HeaderChooseAFolder": "Chọn Một Thư Mục", + "HeaderCollection": "Bộ Sưu Tập", + "HeaderCollectionItems": "Các Mục Bộ Sưu Tập", + "HeaderCover": "Bìa", + "HeaderCurrentDownloads": "Tải Xuống Hiện Tại", + "HeaderCustomMetadataProviders": "Các Nhà Cung Cấp Metadata Tùy Chỉnh", + "HeaderDetails": "Chi Tiết", + "HeaderDownloadQueue": "Hàng Đợi Tải Xuống", + "HeaderEbookFiles": "Tệp Ebook", + "HeaderEmail": "Email", + "HeaderEmailSettings": "Cài Đặt Email", + "HeaderEpisodes": "Tập Phim", + "HeaderEreaderDevices": "Thiết Bị Đọc Sách", + "HeaderEreaderSettings": "Cài Đặt Thiết Bị Đọc Sách", + "HeaderFiles": "Tệp", + "HeaderFindChapters": "Tìm Kiếm Chương", + "HeaderIgnoredFiles": "Tệp Bị Bỏ Qua", + "HeaderItemFiles": "Tệp Mục", + "HeaderItemMetadataUtils": "Công Cụ Metadata Mục", + "HeaderLastListeningSession": "Phiên Nghe Gần Nhất", + "HeaderLatestEpisodes": "Tập Mới Nhất", + "HeaderLibraries": "Thư Viện", + "HeaderLibraryFiles": "Tệp Thư Viện", + "HeaderLibraryStats": "Thống Kê Thư Viện", + "HeaderListeningSessions": "Phiên Nghe", + "HeaderListeningStats": "Thống Kê Nghe", + "HeaderLogin": "Đăng Nhập", + "HeaderLogs": "Nhật Ký", + "HeaderManageGenres": "Quản Lý Thể Loại", + "HeaderManageTags": "Quản Lý Thẻ", + "HeaderMapDetails": "Bản Đồ Chi Tiết", + "HeaderMatch": "Kết Hợp", + "HeaderMetadataOrderOfPrecedence": "Thứ Tự Ưu Tiên Metadata", + "HeaderMetadataToEmbed": "Metadata để nhúng", + "HeaderNewAccount": "Tài Khoản Mới", + "HeaderNewLibrary": "Thư Viện Mới", + "HeaderNotifications": "Thông Báo", + "HeaderOpenIDConnectAuthentication": "Xác Thực Mở ID Connect", + "HeaderOpenRSSFeed": "Mở RSS Feed", + "HeaderOtherFiles": "Các Tệp Khác", + "HeaderPasswordAuthentication": "Xác Thực Mật Khẩu", + "HeaderPermissions": "Quyền Hạn", + "HeaderPlayerQueue": "Hàng Đợi Người Chơi", + "HeaderPlaylist": "Danh Sách Phát", + "HeaderPlaylistItems": "Các Mục Danh Sách Phát", + "HeaderPodcastsToAdd": "Podcasts để Thêm", + "HeaderPreviewCover": "Xem Trước Bìa", + "HeaderRemoveEpisode": "Xóa Tập", + "HeaderRemoveEpisodes": "Xóa {0} Tập", + "HeaderRSSFeedGeneral": "Chi Tiết RSS", + "HeaderRSSFeedIsOpen": "RSS Feed Đã Mở", + "HeaderRSSFeeds": "RSS Feeds", + "HeaderSavedMediaProgress": "Tiến Trình Phương Tiện Đã Lưu", + "HeaderSchedule": "Lịch Trình", + "HeaderScheduleLibraryScans": "Lên Lịch Quét Tự Động Thư Viện", + "HeaderSession": "Phiên", + "HeaderSetBackupSchedule": "Đặt Lịch Sao Lưu", + "HeaderSettings": "Cài Đặt", + "HeaderSettingsDisplay": "Hiển Thị", + "HeaderSettingsExperimental": "Tính Năng Thử Nghiệm", + "HeaderSettingsGeneral": "Chung", + "HeaderSettingsScanner": "Máy Quét", + "HeaderSleepTimer": "Hẹn Giờ Tắt", + "HeaderStatsLargestItems": "Các Mục Lớn Nhất", + "HeaderStatsLongestItems": "Các Mục Dài Nhất (giờ)", + "HeaderStatsMinutesListeningChart": "Thống Kê Thời Gian Nghe (7 ngày gần nhất)", + "HeaderStatsRecentSessions": "Các Phiên Gần Đây", + "HeaderStatsTop10Authors": "10 Tác Giả Hàng Đầu", + "HeaderStatsTop5Genres": "5 Thể Loại Hàng Đầu", + "HeaderTableOfContents": "Mục Lục", + "HeaderTools": "Công Cụ", + "HeaderUpdateAccount": "Cập Nhật Tài Khoản", + "HeaderUpdateAuthor": "Cập Nhật Tác Giả", + "HeaderUpdateDetails": "Cập Nhật Chi Tiết", + "HeaderUpdateLibrary": "Cập Nhật Thư Viện", + "HeaderUsers": "Người Dùng", + "HeaderYearReview": "Năm {0} trong Xem Xét", + "HeaderYourStats": "Thống Kê Của Bạn", + "LabelAbridged": "Rút Gọn", + "LabelAccountType": "Loại Tài Khoản", + "LabelAccountTypeAdmin": "Quản Trị Viên", + "LabelAccountTypeGuest": "Khách", + "LabelAccountTypeUser": "Người Dùng", + "LabelActivity": "Hoạt Động", + "LabelAdded": "Đã Thêm", + "LabelAddedAt": "Đã Thêm Lúc", + "LabelAddToCollection": "Thêm vào Bộ Sưu Tập", + "LabelAddToCollectionBatch": "Thêm {0} Sách vào Bộ Sưu Tập", + "LabelAddToPlaylist": "Thêm vào Danh Sách Phát", + "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", + "LabelAuthorFirstLast": "Author (First Last)", + "LabelAuthorLastFirst": "Author (Last, First)", + "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", + "LabelAutoRegisterDescription": "Automatically create new users after logging in", + "LabelBackToUser": "Back to User", + "LabelBackupLocation": "Backup Location", + "LabelBackupsEnableAutomaticBackups": "Enable automatic backups", + "LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups", + "LabelBackupsMaxBackupSize": "Maximum backup size (in GB)", + "LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.", + "LabelBackupsNumberToKeep": "Number of backups to keep", + "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": "Sách", + "LabelButtonText": "Nút Văn Bản", + "LabelChangePassword": "Đổi Mật Khẩu", + "LabelChannels": "Kênh", + "LabelChapters": "Chương", + "LabelChaptersFound": "chương được tìm thấy", + "LabelChapterTitle": "Tiêu đề Chương", + "LabelClickForMoreInfo": "Nhấn để biết thêm thông tin", + "LabelClosePlayer": "Đóng trình phát", + "LabelCodec": "Mã hóa", + "LabelCollapseSeries": "Thu gọn Series", + "LabelCollection": "Bộ Sưu Tập", + "LabelCollections": "Các Bộ Sưu Tập", + "LabelComplete": "Hoàn Thành", + "LabelConfirmPassword": "Xác Nhận Mật Khẩu", + "LabelContinueListening": "Tiếp Tục Nghe", + "LabelContinueReading": "Tiếp Tục Đọc", + "LabelContinueSeries": "Tiếp Tục Series", + "LabelCover": "Bìa", + "LabelCoverImageURL": "URL Ảnh Bìa", + "LabelCreatedAt": "Được Tạo Lúc", + "LabelCronExpression": "Biểu Thức Cron", + "LabelCurrent": "Hiện tại", + "LabelCurrently": "Hiện tại:", + "LabelCustomCronExpression": "Biểu Thức Cron Tùy Chỉnh:", + "LabelDatetime": "Ngày giờ", + "LabelDeleteFromFileSystemCheckbox": "Xóa khỏi hệ thống tệp (bỏ chọn để chỉ xóa khỏi cơ sở dữ liệu)", + "LabelDescription": "Mô Tả", + "LabelDeselectAll": "Bỏ Chọn Tất Cả", + "LabelDevice": "Thiết Bị", + "LabelDeviceInfo": "Thông Tin Thiết Bị", + "LabelDeviceIsAvailableTo": "Thiết Bị Đã Sẵn Sàng Cho...", + "LabelDirectory": "Thư Mục", + "LabelDiscFromFilename": "Đĩa từ Tên Tệp", + "LabelDiscFromMetadata": "Đĩa từ Metadata", + "LabelDiscover": "Khám Phá", + "LabelDownload": "Tải Xuống", + "LabelDownloadNEpisodes": "Tải Xuống {0} Tập", + "LabelDuration": "Thời Lượng", + "LabelDurationFound": "Thời lượng được tìm thấy:", + "LabelEbook": "Ebook", + "LabelEbooks": "Các Ebook", + "LabelEdit": "Chỉnh Sửa", + "LabelEmail": "Email", + "LabelEmailSettingsFromAddress": "Địa chỉ Gửi từ", + "LabelEmailSettingsSecure": "Bảo Mật", + "LabelEmailSettingsSecureHelp": "Nếu đúng thì kết nối sẽ sử dụng TLS khi kết nối đến máy chủ. Nếu sai thì TLS sẽ được sử dụng nếu máy chủ hỗ trợ phần mở rộng STARTTLS. Trong hầu hết các trường hợp, hãy đặt giá trị này là đúng nếu bạn kết nối đến cổng 465. Đối với cổng 587 hoặc 25, giữ nó sai. (từ nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Địa Chỉ Kiểm Tra", + "LabelEmbeddedCover": "Bìa Nội", + "LabelEnable": "Bật", + "LabelEnd": "Kết Thúc", + "LabelEpisode": "Tập", + "LabelEpisodeTitle": "Tiêu Đề Tập", + "LabelEpisodeType": "Loại Tập", + "LabelExample": "Ví Dụ", + "LabelExplicit": "Rõ Ràng", + "LabelFeedURL": "URL Feed", + "LabelFetchingMetadata": "Đang Lấy Metadata", + "LabelFile": "Tệp", + "LabelFileBirthtime": "Thời Gian Tạo Tệp", + "LabelFileModified": "Sửa Đổi Tệp", + "LabelFilename": "Tên Tệp", + "LabelFilterByUser": "Lọc theo Người Dùng", + "LabelFindEpisodes": "Tìm Tập", + "LabelFinished": "Hoàn Thành", + "LabelFolder": "Thư Mục", + "LabelFolders": "Các Thư Mục", + "LabelFontBold": "Đậm", + "LabelFontFamily": "Gia đình font", + "LabelFontItalic": "Nghiêng", + "LabelFontScale": "Tỷ lệ font", + "LabelFontStrikethrough": "Gạch ngang", + "LabelFormat": "Định dạng", + "LabelGenre": "Thể loại", + "LabelGenres": "Các thể loại", + "LabelHardDeleteFile": "Xóa tập tin vĩnh viễn", + "LabelHasEbook": "Có ebook", + "LabelHasSupplementaryEbook": "Có ebook bổ sung", + "LabelHighestPriority": "Ưu tiên cao nhất", + "LabelHost": "Máy chủ", + "LabelHour": "Giờ", + "LabelIcon": "Biểu tượng", + "LabelImageURLFromTheWeb": "URL hình ảnh từ web", + "LabelIncludeInTracklist": "Bao gồm trong danh sách phát", + "LabelIncomplete": "Chưa hoàn thành", + "LabelInProgress": "Đang tiến hành", + "LabelInterval": "Khoảng cách", + "LabelIntervalCustomDailyWeekly": "Tuỳ chỉnh hàng ngày/hàng tuần", + "LabelIntervalEvery12Hours": "Mỗi 12 giờ", + "LabelIntervalEvery15Minutes": "Mỗi 15 phút", + "LabelIntervalEvery2Hours": "Mỗi 2 giờ", + "LabelIntervalEvery30Minutes": "Mỗi 30 phút", + "LabelIntervalEvery6Hours": "Mỗi 6 giờ", + "LabelIntervalEveryDay": "Mỗi ngày", + "LabelIntervalEveryHour": "Mỗi giờ", + "LabelInvalidParts": "Phần không hợp lệ", + "LabelInvert": "Nghịch đảo", + "LabelItem": "Mục", + "LabelLanguage": "Ngôn ngữ", + "LabelLanguageDefaultServer": "Ngôn ngữ Máy chủ mặc định", + "LabelLastBookAdded": "Sách mới nhất được thêm", + "LabelLastBookUpdated": "Sách mới nhất được cập nhật", + "LabelLastSeen": "Lần cuối nhìn thấy", + "LabelLastTime": "Lần cuối", + "LabelLastUpdate": "Cập nhật cuối cùng", + "LabelLayout": "Bố cục", + "LabelLayoutSinglePage": "Một trang", + "LabelLayoutSplitPage": "Chia trang", + "LabelLess": "Ít hơn", + "LabelLibrariesAccessibleToUser": "Thư viện có thể truy cập cho người dùng", + "LabelLibrary": "Thư viện", + "LabelLibraryItem": "Mục thư viện", + "LabelLibraryName": "Tên thư viện", + "LabelLimit": "Giới hạn", + "LabelLineSpacing": "Khoảng cách dòng", + "LabelListenAgain": "Nghe lại", + "LabelLogLevelDebug": "Gỡ lỗi", + "LabelLogLevelInfo": "Thông tin", + "LabelLogLevelWarn": "Cảnh báo", + "LabelLookForNewEpisodesAfterDate": "Tìm tập mới sau ngày này", + "LabelLowestPriority": "Ưu tiên thấp nhất", + "LabelMatchExistingUsersBy": "Kết hợp người dùng hiện có theo", + "LabelMatchExistingUsersByDescription": "Sử dụng để kết nối người dùng hiện có. Khi kết nối, người dùng sẽ được kết hợp bằng một ID duy nhất từ nhà cung cấp SSO của bạn", + "LabelMediaPlayer": "Trình phát đa phương tiện", + "LabelMediaType": "Loại phương tiện", + "LabelMetadataOrderOfPrecedenceDescription": "Nguồn siêu dữ liệu ưu tiên cao hơn sẽ ghi đè lên các nguồn siêu dữ liệu ưu tiên thấp hơn", + "LabelMetadataProvider": "Nhà cung cấp siêu dữ liệu", + "LabelMetaTag": "Thẻ Meta", + "LabelMetaTags": "Các thẻ Meta", + "LabelMinute": "Phút", + "LabelMissing": "Thiếu", + "LabelMissingEbook": "Không có ebook", + "LabelMissingParts": "Các phần thiếu", + "LabelMissingSupplementaryEbook": "Không có ebook bổ sung", + "LabelMobileRedirectURIs": "URI chuyển hướng di động được cho phép", + "LabelMobileRedirectURIsDescription": "Đây là danh sách trắng các URI chuyển hướng hợp lệ cho ứng dụng di động. Mặc định là <code>audiobookshelf://oauth</code>, bạn có thể loại bỏ hoặc bổ sung thêm các URI cho tích hợp ứng dụng bên thứ ba. Sử dụng dấu hoa thị (<code>*</code>) như một mục duy nhất cho phép bất kỳ URI nào.", + "LabelMore": "Thêm", + "LabelMoreInfo": "Thông tin thêm", + "LabelName": "Tên", + "LabelNarrator": "Người kể", + "LabelNarrators": "Các người kể", + "LabelNew": "Mới", + "LabelNewestAuthors": "Nhà văn mới nhất", + "LabelNewestEpisodes": "Tập mới nhất", + "LabelNewPassword": "Mật khẩu mới", + "LabelNextBackupDate": "Ngày sao lưu tiếp theo", + "LabelNextScheduledRun": "Chạy tiếp theo theo lịch trình", + "LabelNoEpisodesSelected": "Không có tập nào được chọn", + "LabelNotes": "Ghi chú", + "LabelNotFinished": "Chưa hoàn thành", + "LabelNotificationAppriseURL": "URL(s) thông báo", + "LabelNotificationAvailableVariables": "Biến có sẵn", + "LabelNotificationBodyTemplate": "Mẫu Nội dung", + "LabelNotificationEvent": "Sự kiện Thông báo", + "LabelNotificationsMaxFailedAttempts": "Số lần thất bại tối đa", + "LabelNotificationsMaxFailedAttemptsHelp": "Thông báo sẽ bị vô hiệu hóa sau khi thất bại gửi số lần này", + "LabelNotificationsMaxQueueSize": "Kích thước hàng đợi tối đa cho sự kiện thông báo", + "LabelNotificationsMaxQueueSizeHelp": "Các sự kiện bị giới hạn mỗi giây chỉ gửi 1 lần. Các sự kiện sẽ bị bỏ qua nếu hàng đợi đạt kích thước tối đa. Điều này ngăn chặn spam thông báo.", + "LabelNotificationTitleTemplate": "Mẫu Tiêu đề", + "LabelNotStarted": "Chưa bắt đầu", + "LabelNumberOfBooks": "Số lượng Sách", + "LabelNumberOfEpisodes": "# của Tập", + "LabelOpenRSSFeed": "Mở RSS Feed", + "LabelOverwrite": "Ghi đè", + "LabelPassword": "Mật khẩu", + "LabelPath": "Đường dẫn", + "LabelPermissionsAccessAllLibraries": "Có Thể Truy Cập Tất Cả Thư Viện", + "LabelPermissionsAccessAllTags": "Có Thể Truy Cập Tất Cả Thẻ", + "LabelPermissionsAccessExplicitContent": "Có Thể Truy Cập Nội Dung Rõ Ràng", + "LabelPermissionsDelete": "Có Thể Xóa", + "LabelPermissionsDownload": "Có Thể Tải Xuống", + "LabelPermissionsUpdate": "Có Thể Cập Nhật", + "LabelPermissionsUpload": "Có Thể Tải Lên", + "LabelPersonalYearReview": "Năm của Bạn trong Bài Đánh Giá ({0})", + "LabelPhotoPathURL": "Đường dẫn/URL ảnh", + "LabelPlaylists": "Danh sách phát", + "LabelPlayMethod": "Phương pháp phát", + "LabelPodcast": "Podcast", + "LabelPodcasts": "Các podcast", + "LabelPodcastSearchRegion": "Vùng tìm kiếm podcast", + "LabelPodcastType": "Loại Podcast", + "LabelPort": "Cổng", + "LabelPrefixesToIgnore": "Tiền tố để bỏ qua (không phân biệt chữ hoa/chữ thường)", + "LabelPreventIndexing": "Ngăn chặn feed của bạn được chỉ mục bởi thư mục podcast của iTunes và Google", + "LabelPrimaryEbook": "Ebook chính", + "LabelProgress": "Tiến độ", + "LabelProvider": "Nhà cung cấp", + "LabelPubDate": "Ngày Xuất bản", + "LabelPublisher": "Nhà xuất bản", + "LabelPublishYear": "Năm Xuất bản", + "LabelRead": "Đọc", + "LabelReadAgain": "Đọc lại", + "LabelReadEbookWithoutProgress": "Đọc ebook mà không giữ tiến độ", + "LabelRecentlyAdded": "Gần đây thêm vào", + "LabelRecentSeries": "Loạt phim gần đây", + "LabelRecommended": "Được khuyến nghị", + "LabelRedo": "Làm lại", + "LabelRegion": "Khu vực", + "LabelReleaseDate": "Ngày Phát hành", + "LabelRemoveCover": "Xóa ảnh bìa", + "LabelRowsPerPage": "Số dòng mỗi trang", + "LabelRSSFeedCustomOwnerEmail": "Email chủ sở hữu tùy chỉnh", + "LabelRSSFeedCustomOwnerName": "Tên chủ sở hữu tùy chỉnh", + "LabelRSSFeedOpen": "Mở RSS Feed", + "LabelRSSFeedPreventIndexing": "Ngăn chặn Chỉ mục RSS Feed", + "LabelRSSFeedSlug": "Slug RSS Feed", + "LabelRSSFeedURL": "URL RSS Feed", + "LabelSearchTerm": "Thuật ngữ tìm kiếm", + "LabelSearchTitle": "Tìm kiếm Tiêu đề", + "LabelSearchTitleOrASIN": "Tìm kiếm Tiêu đề hoặc ASIN", + "LabelSeason": "Mùa", + "LabelSelectAllEpisodes": "Chọn tất cả các tập", + "LabelSelectEpisodesShowing": "Chọn {0} tập đang hiển thị", + "LabelSelectUsers": "Chọn người dùng", + "LabelSendEbookToDevice": "Gửi Ebook tới...", + "LabelSequence": "Trình tự", + "LabelSeries": "Loạt", + "LabelSeriesName": "Tên loạt", + "LabelSeriesProgress": "Tiến độ loạt", + "LabelServerYearReview": "Năm của Máy chủ trong Bài Đánh Giá ({0})", + "LabelSetEbookAsPrimary": "Đặt làm chính", + "LabelSetEbookAsSupplementary": "Đặt là bổ sung", + "LabelSettingsAudiobooksOnly": "Chỉ sách nói", + "LabelSettingsAudiobooksOnlyHelp": "Bật cài đặt này sẽ bỏ qua các tập tin ebook trừ khi chúng ở trong một thư mục sách nói, trong trường hợp đó chúng sẽ được đặt làm ebook bổ sung", + "LabelSettingsBookshelfViewHelp": "Thiết kế giả lập với kệ gỗ", + "LabelSettingsChromecastSupport": "Hỗ trợ Chromecast", + "LabelSettingsDateFormat": "Định dạng Ngày", + "LabelSettingsDisableWatcher": "Tắt Watcher", + "LabelSettingsDisableWatcherForLibrary": "Tắt watcher thư mục cho thư viện", + "LabelSettingsDisableWatcherHelp": "Tắt chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ", + "LabelSettingsEnableWatcher": "Bật Watcher", + "LabelSettingsEnableWatcherForLibrary": "Bật watcher thư mục cho thư viện", + "LabelSettingsEnableWatcherHelp": "Bật chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ", + "LabelSettingsExperimentalFeatures": "Tính năng thử nghiệm", + "LabelSettingsExperimentalFeaturesHelp": "Các tính năng đang phát triển có thể cần phản hồi của bạn và sự giúp đỡ trong thử nghiệm. Nhấp để mở thảo luận trên github.", + "LabelSettingsFindCovers": "Tìm ảnh bìa", + "LabelSettingsFindCoversHelp": "Nếu sách nói của bạn không có ảnh bìa nhúng hoặc ảnh bìa trong thư mục, trình quét sẽ cố gắng tìm ảnh bìa.<br>Lưu ý: Điều này sẽ kéo dài thời gian quét", + "LabelSettingsHideSingleBookSeries": "Ẩn loạt sách đơn lẻ", + "LabelSettingsHideSingleBookSeriesHelp": "Các loạt sách chỉ có một cuốn sách sẽ được ẩn khỏi trang loạt sách và kệ trang chủ.", + "LabelSettingsHomePageBookshelfView": "Trang chủ sử dụng chế độ xem kệ sách", + "LabelSettingsLibraryBookshelfView": "Thư viện sử dụng chế độ xem kệ sách", + "LabelSettingsParseSubtitles": "Phân tích phụ đề", + "LabelSettingsParseSubtitlesHelp": "Trích xuất phụ đề từ tên thư mục sách nói.<br>Phụ đề phải được tách bằng \" - \"<br>i.e. \"Book Title - A Subtitle Here\" có phụ đề \"A Subtitle Here\"", + "LabelSettingsPreferMatchedMetadata": "Ưu tiên siêu dữ liệu phù hợp", + "LabelSettingsPreferMatchedMetadataHelp": "Dữ liệu phù hợp sẽ ghi đè lên chi tiết mục khi sử dụng Kết hợp Nhanh. Theo mặc định, Kết hợp Nhanh chỉ điền vào các chi tiết bị thiếu.", + "LabelSettingsSkipMatchingBooksWithASIN": "Bỏ qua sách khớp có ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Bỏ qua sách khớp có ISBN", + "LabelSettingsSortingIgnorePrefixes": "Bỏ qua tiền tố khi sắp xếp", + "LabelSettingsSortingIgnorePrefixesHelp": "ví dụ. với tiền tố \"the\" tiêu đề sách \"The Book Title\" sẽ được sắp xếp như \"Book Title, The\"", + "LabelSettingsSquareBookCovers": "Sử dụng ảnh bìa vuông", + "LabelSettingsSquareBookCoversHelp": "Ưu tiên sử dụng ảnh bìa vuông hơn ảnh bìa tiêu chuẩn 1.6:1", + "LabelSettingsStoreCoversWithItem": "Lưu trữ ảnh bìa với mục", + "LabelSettingsStoreCoversWithItemHelp": "Theo mặc định, ảnh bìa được lưu trữ trong /metadata/items, bật cài đặt này sẽ lưu trữ ảnh bìa trong thư mục mục của thư viện bạn. Chỉ một tệp có tên là \"cover\" sẽ được giữ lại", + "LabelSettingsStoreMetadataWithItem": "Lưu trữ siêu dữ liệu với mục", + "LabelSettingsStoreMetadataWithItemHelp": "Theo mặc định, các tệp siêu dữ liệu được lưu trữ trong /metadata/items, bật cài đặt này sẽ lưu trữ các tệp siêu dữ liệu trong các thư mục mục của thư viện bạn", + "LabelSettingsTimeFormat": "Định dạng Thời gian", + "LabelShowAll": "Hiển thị Tất cả", + "LabelSize": "Kích thước", + "LabelSleepTimer": "Hẹn giờ tắt", + "LabelSlug": "Slug", + "LabelStart": "Bắt đầu", + "LabelStarted": "Đã bắt đầu", + "LabelStartedAt": "Bắt đầu vào", + "LabelStartTime": "Thời gian bắt đầu", + "LabelStatsAudioTracks": "Audio Tracks", + "LabelStatsAuthors": "Tác giả", + "LabelStatsBestDay": "Ngày tốt nhất", + "LabelStatsDailyAverage": "Trung bình hàng ngày", + "LabelStatsDays": "Ngày", + "LabelStatsDaysListened": "Ngày đã nghe", + "LabelStatsHours": "Giờ", + "LabelStatsInARow": "liên tiếp", + "LabelStatsItemsFinished": "Mục đã hoàn thành", + "LabelStatsItemsInLibrary": "Mục trong thư viện", + "LabelStatsMinutes": "phút", + "LabelStatsMinutesListening": "Phút Nghe", + "LabelStatsOverallDays": "Tổng số ngày", + "LabelStatsOverallHours": "Tổng số giờ", + "LabelStatsWeekListening": "Tuần nghe", + "LabelSubtitle": "Phụ đề", + "LabelSupportedFileTypes": "Loại tệp được hỗ trợ", + "LabelTag": "Thẻ", + "LabelTags": "Thẻ", + "LabelTagsAccessibleToUser": "Thẻ Có Thể Truy Cập Cho Người Dùng", + "LabelTagsNotAccessibleToUser": "Thẻ Không Thể Truy Cập Cho Người Dùng", + "LabelTasks": "Nhiệm vụ Đang chạy", + "LabelTextEditorBulletedList": "Danh sách có dấu đầu dòng", + "LabelTextEditorLink": "Liên kết", + "LabelTextEditorNumberedList": "Danh sách đánh số", + "LabelTextEditorUnlink": "Gỡ liên kết", + "LabelTheme": "Chủ đề", + "LabelThemeDark": "Tối", + "LabelThemeLight": "Sáng", + "LabelTimeBase": "Thời gian cơ bản", + "LabelTimeListened": "Thời gian đã nghe", + "LabelTimeListenedToday": "Thời gian đã nghe hôm nay", + "LabelTimeRemaining": "{0} còn lại", + "LabelTimeToShift": "Thời gian dời chuyển theo giây", + "LabelTitle": "Tiêu đề", + "LabelToolsEmbedMetadata": "Nhúng siêu dữ liệu", + "LabelToolsEmbedMetadataDescription": "Nhúng siêu dữ liệu vào tệp âm thanh bao gồm ảnh bìa và chương.", + "LabelToolsMakeM4b": "Tạo Tệp Audiobook M4B", + "LabelToolsMakeM4bDescription": "Tạo tệp audiobook .M4B với siêu dữ liệu nhúng, ảnh bìa và chương.", + "LabelToolsSplitM4b": "Chia M4B thành MP3", + "LabelToolsSplitM4bDescription": "Tạo MP3 từ M4B được chia theo chương với siêu dữ liệu nhúng, ảnh bìa và chương.", + "LabelTotalDuration": "Tổng thời lượng", + "LabelTotalTimeListened": "Tổng thời gian đã nghe", + "LabelTrackFromFilename": "Từ tên tệp", + "LabelTrackFromMetadata": "Từ siêu dữ liệu", + "LabelTracks": "Bài hát", + "LabelTracksMultiTrack": "Nhiều track", + "LabelTracksNone": "Không có track", + "LabelTracksSingleTrack": "Một track", + "LabelType": "Loại", + "LabelUnabridged": "Không rút gọn", + "LabelUndo": "Hoàn tác", + "LabelUnknown": "Không xác định", + "LabelUpdateCover": "Cập nhật ảnh bìa", + "LabelUpdateCoverHelp": "Cho phép ghi đè lên các ảnh bìa hiện có cho các cuốn sách được chọn khi tìm thấy một kết hợp", + "LabelUpdatedAt": "Cập nhật lúc", + "LabelUpdateDetails": "Cập nhật chi tiết", + "LabelUpdateDetailsHelp": "Cho phép ghi đè lên các chi tiết hiện có cho các cuốn sách được chọn khi tìm thấy một kết hợp", + "LabelUploaderDragAndDrop": "Kéo và thả tệp hoặc thư mục", + "LabelUploaderDropFiles": "Thả tệp", + "LabelUploaderItemFetchMetadataHelp": "Tự động lấy tiêu đề, tác giả và loạt", + "LabelUseChapterTrack": "Sử dụng track chương", + "LabelUseFullTrack": "Sử dụng toàn bộ track", + "LabelUser": "Người dùng", + "LabelUsername": "Tên người dùng", + "LabelValue": "Giá trị", + "LabelVersion": "Phiên bản", + "LabelViewBookmarks": "Xem các đánh dấu", + "LabelViewChapters": "Xem các chương", + "LabelViewQueue": "Xem hàng đợi phát", + "LabelVolume": "Âm lượng", + "LabelWeekdaysToRun": "Ngày trong tuần để chạy", + "LabelYearReviewHide": "Ẩn Năm trong Bài Đánh Giá", + "LabelYearReviewShow": "Xem Năm trong Bài Đánh Giá", + "LabelYourAudiobookDuration": "Thời lượng sách nói của bạn", + "LabelYourBookmarks": "Đánh dấu của bạn", + "LabelYourPlaylists": "Danh sách phát của bạn", + "LabelYourProgress": "Tiến trình của bạn", + "MessageAddToPlayerQueue": "Thêm vào hàng đợi phát", + "MessageAppriseDescription": "Để sử dụng tính năng này, bạn cần có một phiên bản của <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> đang chạy hoặc một api sẽ xử lý các yêu cầu tương tự. <br /> Địa chỉ URL của Apprise API nên là đường dẫn URL đầy đủ để gửi thông báo, ví dụ, nếu phiên bản API của bạn được phục vụ tại <code>http://192.168.1.1:8337</code> thì bạn sẽ đặt <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "Bản sao bao gồm người dùng, tiến độ của người dùng, chi tiết mục thư viện, cài đặt máy chủ và hình ảnh được lưu trữ trong <code>/metadata/items</code> & <code>/metadata/authors</code>. Bản sao <strong>không</strong> bao gồm bất kỳ tệp nào được lưu trữ trong các thư mục thư viện của bạn.", + "MessageBatchQuickMatchDescription": "Quick Match sẽ cố gắng thêm các ảnh bìa và siêu dữ liệu bị thiếu cho các mục đã chọn. Bật các tùy chọn dưới đây để cho phép Quick Match ghi đè lên các ảnh bìa hiện có và / hoặc siêu dữ liệu.", + "MessageBookshelfNoCollections": "Bạn chưa tạo bất kỳ bộ sưu tập nào", + "MessageBookshelfNoResultsForFilter": "Không có Kết quả cho bộ lọc \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "Không có nguồn cung cấp RSS nào đang mở", + "MessageBookshelfNoSeries": "Bạn không có bộ sách", + "MessageChapterEndIsAfter": "Kết thúc chương sau khi kết thúc sách nói của bạn", + "MessageChapterErrorFirstNotZero": "Chương đầu tiên phải bắt đầu từ 0", + "MessageChapterErrorStartGteDuration": "Thời gian bắt đầu không hợp lệ phải nhỏ hơn thời lượng sách nói", + "MessageChapterErrorStartLtPrev": "Thời gian bắt đầu không hợp lệ phải lớn hơn hoặc bằng thời gian bắt đầu của chương trước", + "MessageChapterStartIsAfter": "Bắt đầu chương sau khi kết thúc sách nói của bạn", + "MessageCheckingCron": "Kiểm tra cron...", + "MessageConfirmCloseFeed": "Bạn có chắc chắn muốn đóng nguồn cung cấp này không?", + "MessageConfirmDeleteBackup": "Bạn có chắc chắn muốn xóa bản sao lưu cho {0} không?", + "MessageConfirmDeleteFile": "Điều này sẽ xóa tệp khỏi hệ thống tệp của bạn. Bạn có chắc chắn không?", + "MessageConfirmDeleteLibrary": "Bạn có chắc chắn muốn xóa vĩnh viễn thư viện \"{0}\" không?", + "MessageConfirmDeleteLibraryItem": "Điều này sẽ xóa mục thư viện khỏi cơ sở dữ liệu và hệ thống tệp của bạn. Bạn có chắc chắn không?", + "MessageConfirmDeleteLibraryItems": "Điều này sẽ xóa {0} mục thư viện khỏi cơ sở dữ liệu và hệ thống tệp của bạn. Bạn có chắc chắn không?", + "MessageConfirmDeleteSession": "Bạn có chắc chắn muốn xóa phiên này không?", + "MessageConfirmForceReScan": "Bạn có chắc chắn muốn buộc quét lại không?", + "MessageConfirmMarkAllEpisodesFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các tập phim đã kết thúc không?", + "MessageConfirmMarkAllEpisodesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các tập phim chưa kết thúc không?", + "MessageConfirmMarkSeriesFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này đã kết thúc không?", + "MessageConfirmMarkSeriesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này chưa kết thúc không?", + "MessageConfirmQuickEmbed": "Cảnh báo! Quick embed sẽ không sao lưu các tệp âm thanh của bạn. Đảm bảo bạn có một bản sao lưu của các tệp âm thanh của bạn. <br><br>Bạn có muốn tiếp tục không?", + "MessageConfirmRemoveAllChapters": "Bạn có chắc chắn muốn xóa tất cả các chương không?", + "MessageConfirmRemoveAuthor": "Bạn có chắc chắn muốn xóa tác giả \"{0}\" không?", + "MessageConfirmRemoveCollection": "Bạn có chắc chắn muốn xóa bộ sưu tập \"{0}\" không?", + "MessageConfirmRemoveEpisode": "Bạn có chắc chắn muốn xóa tập phim \"{0}\" không?", + "MessageConfirmRemoveEpisodes": "Bạn có chắc chắn muốn xóa {0} tập phim không?", + "MessageConfirmRemoveListeningSessions": "Bạn có chắc chắn muốn xóa {0} phiên nghe không?", + "MessageConfirmRemoveNarrator": "Bạn có chắc chắn muốn xóa người kể chuyện \"{0}\" không?", + "MessageConfirmRemovePlaylist": "Bạn có chắc chắn muốn xóa danh sách phát của bạn \"{0}\" không?", + "MessageConfirmRenameGenre": "Bạn có chắc chắn muốn đổi tên thể loại \"{0}\" thành \"{1}\" cho tất cả các mục không?", + "MessageConfirmRenameGenreMergeNote": "Lưu ý: Thể loại này đã tồn tại nên chúng sẽ được hợp nhất.", + "MessageConfirmRenameGenreWarning": "Cảnh báo! Một thể loại tương tự với kiểu chữ khác đã tồn tại \"{0}\".", + "MessageConfirmRenameTag": "Bạn có chắc chắn muốn đổi tên tag \"{0}\" thành \"{1}\" cho tất cả các mục không?", + "MessageConfirmRenameTagMergeNote": "Lưu ý: Thẻ này đã tồn tại nên chúng sẽ được hợp nhất.", + "MessageConfirmRenameTagWarning": "Cảnh báo! Một thẻ tương tự với kiểu chữ khác đã tồn tại \"{0}\".", + "MessageConfirmReScanLibraryItems": "Bạn có chắc chắn muốn quét lại {0} mục không?", + "MessageConfirmSendEbookToDevice": "Bạn có chắc chắn muốn gửi {0} ebook \"{1}\" đến thiết bị \"{2}\" không?", + "MessageDownloadingEpisode": "Đang tải tập phim", + "MessageDragFilesIntoTrackOrder": "Kéo tệp vào thứ tự track đúng", + "MessageEmbedFinished": "Nhúng Hoàn thành!", + "MessageEpisodesQueuedForDownload": "{0} Tập(s) đã được thêm vào hàng đợi để tải xuống", + "MessageFeedURLWillBe": "URL nguồn cấp sẽ là {0}", + "MessageFetching": "Đang tìm...", + "MessageForceReScanDescription": "sẽ quét lại tất cả các tệp như một quét mới. Các thẻ ID3 của tệp âm thanh, tệp OPF và tệp văn bản sẽ được quét làm mới.", + "MessageImportantNotice": "Thông báo quan trọng!", + "MessageInsertChapterBelow": "Chèn chương dưới đây", + "MessageItemsSelected": "{0} Mục Đã Chọn", + "MessageItemsUpdated": "{0} Mục Đã Cập Nhật", + "MessageJoinUsOn": "Tham gia cùng chúng tôi trên", + "MessageListeningSessionsInTheLastYear": "{0} phiên nghe trong năm qua", + "MessageLoading": "Đang tải...", + "MessageLoadingFolders": "Đang tải các thư mục...", + "MessageM4BFailed": "M4B thất bại!", + "MessageM4BFinished": "M4B Hoàn thành!", + "MessageMapChapterTitles": "Ánh xạ tiêu đề chương với các chương hiện có của sách audio của bạn mà không điều chỉnh thời gian", + "MessageMarkAllEpisodesFinished": "Đánh dấu tất cả các tập phim đã kết thúc", + "MessageMarkAllEpisodesNotFinished": "Đánh dấu tất cả các tập phim chưa kết thúc", + "MessageMarkAsFinished": "Đánh dấu là Đã Kết Thúc", + "MessageMarkAsNotFinished": "Đánh dấu là Chưa Kết Thúc", + "MessageMatchBooksDescription": "sẽ cố gắng kết hợp các sách trong thư viện với một cuốn sách từ nhà cung cấp tìm kiếm được chọn và điền vào các chi tiết trống và ảnh bìa. Không ghi đè các chi tiết.", + "MessageNoAudioTracks": "Không có track âm thanh", + "MessageNoAuthors": "Không có Tác giả", + "MessageNoBackups": "Không có Bản sao lưu", + "MessageNoBookmarks": "Không có Đánh dấu", + "MessageNoChapters": "Không có Chương", + "MessageNoCollections": "Không có Bộ sưu tập", + "MessageNoCoversFound": "Không tìm thấy Ảnh bìa", + "MessageNoDescription": "Không có mô tả", + "MessageNoDownloadsInProgress": "Không có tải xuống đang tiến hành", + "MessageNoDownloadsQueued": "Không có tải xuống được xếp hàng", + "MessageNoEpisodeMatchesFound": "Không tìm thấy tập phim nào phù hợp", + "MessageNoEpisodes": "Không có Tập phim", + "MessageNoFoldersAvailable": "Không có Thư mục nào có sẵn", + "MessageNoGenres": "Không có Thể loại", + "MessageNoIssues": "Không có Vấn đề", + "MessageNoItems": "Không có Mục", + "MessageNoItemsFound": "Không tìm thấy mục nào", + "MessageNoListeningSessions": "Không có Phiên Nghe", + "MessageNoLogs": "Không có Log", + "MessageNoMediaProgress": "Không có Tiến độ Phương tiện", + "MessageNoNotifications": "Không có Thông báo", + "MessageNoPodcastsFound": "Không tìm thấy podcast nào", + "MessageNoResults": "Không có Kết quả", + "MessageNoSearchResultsFor": "Không có kết quả tìm kiếm cho \"{0}\"", + "MessageNoSeries": "Không có Bộ", + "MessageNoTags": "Không có Thẻ", + "MessageNoTasksRunning": "Không có Công việc đang chạy", + "MessageNotYetImplemented": "Chưa được triển khai", + "MessageNoUpdateNecessary": "Không cần cập nhật", + "MessageNoUpdatesWereNecessary": "Không cần cập nhật", + "MessageNoUserPlaylists": "Bạn chưa có danh sách phát", + "MessageOr": "hoặc", + "MessagePauseChapter": "Tạm dừng phát chương", + "MessagePlayChapter": "Nghe từ đầu chương", + "MessagePlaylistCreateFromCollection": "Tạo danh sách phát từ bộ sưu tập", + "MessagePodcastHasNoRSSFeedForMatching": "Podcast không có RSS feed để sử dụng cho việc kết hợp", + "MessageQuickMatchDescription": "Điền chi tiết mục trống và ảnh bìa với kết quả phù hợp đầu tiên từ '{0}'. Không ghi đè chi tiết trừ khi cài đặt máy chủ 'Ưu tiên dữ liệu phù hợp' được bật.", + "MessageRemoveChapter": "Xóa chương", + "MessageRemoveEpisodes": "Xóa {0} tập", + "MessageRemoveFromPlayerQueue": "Xóa khỏi hàng đợi phát", + "MessageRemoveUserWarning": "Bạn có chắc chắn muốn xóa người dùng \"{0}\" một cách vĩnh viễn không?", + "MessageReportBugsAndContribute": "Báo cáo lỗi, yêu cầu tính năng và đóng góp tại", + "MessageResetChaptersConfirm": "Bạn có chắc chắn muốn đặt lại các chương và hủy những thay đổi bạn đã thực hiện không?", + "MessageRestoreBackupConfirm": "Bạn có chắc chắn muốn khôi phục bản sao lưu được tạo vào", + "MessageRestoreBackupWarning": "Việc khôi phục bản sao lưu sẽ ghi đè lên toàn bộ cơ sở dữ liệu được đặt tại /config và ảnh bìa trong /metadata/items & /metadata/authors.<br /><br />Bản sao lưu không sửa đổi bất kỳ tệp nào trong các thư mục thư viện của bạn. Nếu bạn đã bật các cài đặt máy chủ để lưu ảnh bìa và dữ liệu phần mềm trong các thư mục thư viện của mình thì chúng sẽ không được sao lưu hoặc ghi đè.<br /><br />Tất cả các máy khách sử dụng máy chủ của bạn sẽ được làm mới tự động.", + "MessageSearchResultsFor": "Kết quả tìm kiếm cho", + "MessageSelected": "{0} đã được chọn", + "MessageServerCouldNotBeReached": "Không thể kết nối đến máy chủ", + "MessageSetChaptersFromTracksDescription": "Đặt chương sử dụng mỗi tệp âm thanh là một chương và tiêu đề chương là tên tệp âm thanh", + "MessageStartPlaybackAtTime": "Bắt đầu phát \"{0}\" tại thời điểm {1}?", + "MessageThinking": "Đang suy nghĩ...", + "MessageUploaderItemFailed": "Không thể tải lên", + "MessageUploaderItemSuccess": "Tải lên thành công!", + "MessageUploading": "Đang tải lên...", + "MessageValidCronExpression": "Biểu thức cron hợp lệ", + "MessageWatcherIsDisabledGlobally": "Watcher đã bị vô hiệu hóa toàn cầu trong cài đặt máy chủ", + "MessageXLibraryIsEmpty": "Thư viện {0} rỗng!", + "MessageYourAudiobookDurationIsLonger": "Thời lượng sách nói của bạn dài hơn so với thời lượng tìm thấy", + "MessageYourAudiobookDurationIsShorter": "Thời lượng sách nói của bạn ngắn hơn so với thời lượng tìm thấy", + "NoteChangeRootPassword": "Người dùng Root là người dùng duy nhất có thể có mật khẩu trống", + "NoteChapterEditorTimes": "Lưu ý: Thời gian bắt đầu của chương đầu tiên phải ở 0:00 và thời gian bắt đầu của chương cuối cùng không thể vượt quá thời lượng của sách nói này.", + "NoteFolderPicker": "Lưu ý: các thư mục đã được ánh xạ trước đó sẽ không được hiển thị", + "NoteRSSFeedPodcastAppsHttps": "Cảnh báo: Hầu hết các ứng dụng podcast sẽ yêu cầu URL của RSS feed sử dụng HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "Cảnh báo: 1 hoặc nhiều tập của bạn không có Pub Date. Một số ứng dụng podcast yêu cầu điều này.", + "NoteUploaderFoldersWithMediaFiles": "Các thư mục có tệp phương tiện sẽ được xử lý như các mục thư viện riêng biệt.", + "NoteUploaderOnlyAudioFiles": "Nếu chỉ tải lên các tệp âm thanh thì mỗi tệp âm thanh sẽ được xử lý như một cuốn sách nói riêng biệt.", + "NoteUploaderUnsupportedFiles": "Các tệp không được hỗ trợ sẽ bị bỏ qua. Khi chọn hoặc thả một thư mục, các tệp khác không có trong thư mục mục sẽ bị bỏ qua.", + "PlaceholderNewCollection": "Tên bộ sưu tập mới", + "PlaceholderNewFolderPath": "Đường dẫn thư mục mới", + "PlaceholderNewPlaylist": "Tên danh sách phát mới", + "PlaceholderSearch": "Tìm kiếm..", + "PlaceholderSearchEpisode": "Tìm kiếm tập..", + "ToastAccountUpdateFailed": "Cập nhật tài khoản thất bại", + "ToastAccountUpdateSuccess": "Tài khoản đã được cập nhật", + "ToastAuthorImageRemoveFailed": "Không thể xóa ảnh tác giả", + "ToastAuthorImageRemoveSuccess": "Ảnh tác giả đã được xóa", + "ToastAuthorUpdateFailed": "Cập nhật tác giả thất bại", + "ToastAuthorUpdateMerged": "Tác giả đã được hợp nhất", + "ToastAuthorUpdateSuccess": "Cập nhật tác giả thành công", + "ToastAuthorUpdateSuccessNoImageFound": "Cập nhật tác giả thành công (không tìm thấy ảnh)", + "ToastBackupCreateFailed": "Tạo bản sao lưu thất bại", + "ToastBackupCreateSuccess": "Bản sao lưu được tạo", + "ToastBackupDeleteFailed": "Xóa bản sao lưu thất bại", + "ToastBackupDeleteSuccess": "Bản sao lưu đã được xóa", + "ToastBackupRestoreFailed": "Khôi phục bản sao lưu thất bại", + "ToastBackupUploadFailed": "Tải lên bản sao lưu thất bại", + "ToastBackupUploadSuccess": "Bản sao lưu đã được tải lên", + "ToastBatchUpdateFailed": "Cập nhật nhóm thất bại", + "ToastBatchUpdateSuccess": "Cập nhật nhóm thành công", + "ToastBookmarkCreateFailed": "Tạo đánh dấu thất bại", + "ToastBookmarkCreateSuccess": "Đã thêm đánh dấu", + "ToastBookmarkRemoveFailed": "Xóa đánh dấu thất bại", + "ToastBookmarkRemoveSuccess": "Đánh dấu đã được xóa", + "ToastBookmarkUpdateFailed": "Cập nhật đánh dấu thất bại", + "ToastBookmarkUpdateSuccess": "Đánh dấu đã được cập nhật", + "ToastChaptersHaveErrors": "Các chương có lỗi", + "ToastChaptersMustHaveTitles": "Các chương phải có tiêu đề", + "ToastCollectionItemsRemoveFailed": "Xóa mục từ bộ sưu tập thất bại", + "ToastCollectionItemsRemoveSuccess": "Mục đã được xóa khỏi bộ sưu tập", + "ToastCollectionRemoveFailed": "Xóa bộ sưu tập thất bại", + "ToastCollectionRemoveSuccess": "Bộ sưu tập đã được xóa", + "ToastCollectionUpdateFailed": "Cập nhật bộ sưu tập thất bại", + "ToastCollectionUpdateSuccess": "Bộ sưu tập đã được cập nhật", + "ToastItemCoverUpdateFailed": "Cập nhật ảnh bìa mục thất bại", + "ToastItemCoverUpdateSuccess": "Ảnh bìa mục đã được cập nhật", + "ToastItemDetailsUpdateFailed": "Cập nhật chi tiết mục thất bại", + "ToastItemDetailsUpdateSuccess": "Chi tiết mục đã được cập nhật", + "ToastItemDetailsUpdateUnneeded": "Không cần cập nhật chi tiết mục", + "ToastItemMarkedAsFinishedFailed": "Đánh dấu mục là Hoàn thành thất bại", + "ToastItemMarkedAsFinishedSuccess": "Mục đã được đánh dấu là Hoàn thành", + "ToastItemMarkedAsNotFinishedFailed": "Đánh dấu mục là Chưa hoàn thành thất bại", + "ToastItemMarkedAsNotFinishedSuccess": "Mục đã được đánh dấu là Chưa hoàn thành", + "ToastLibraryCreateFailed": "Tạo thư viện thất bại", + "ToastLibraryCreateSuccess": "Thư viện \"{0}\" đã được tạo", + "ToastLibraryDeleteFailed": "Xóa thư viện thất bại", + "ToastLibraryDeleteSuccess": "Thư viện đã được xóa", + "ToastLibraryScanFailedToStart": "Không thể bắt đầu quét thư viện", + "ToastLibraryScanStarted": "Quét thư viện đã được bắt đầu", + "ToastLibraryUpdateFailed": "Cập nhật thư viện thất bại", + "ToastLibraryUpdateSuccess": "Thư viện \"{0}\" đã được cập nhật", + "ToastPlaylistCreateFailed": "Tạo danh sách phát thất bại", + "ToastPlaylistCreateSuccess": "Danh sách phát đã được tạo", + "ToastPlaylistRemoveFailed": "Xóa danh sách phát thất bại", + "ToastPlaylistRemoveSuccess": "Danh sách phát đã được xóa", + "ToastPlaylistUpdateFailed": "Cập nhật danh sách phát thất bại", + "ToastPlaylistUpdateSuccess": "Danh sách phát đã được cập nhật", + "ToastPodcastCreateFailed": "Tạo podcast thất bại", + "ToastPodcastCreateSuccess": "Podcast đã được tạo thành công", + "ToastRemoveItemFromCollectionFailed": "Xóa mục khỏi bộ sưu tập thất bại", + "ToastRemoveItemFromCollectionSuccess": "Mục đã được xóa khỏi bộ sưu tập", + "ToastRSSFeedCloseFailed": "Đóng nguồn cấp RSS thất bại", + "ToastRSSFeedCloseSuccess": "Nguồn cấp RSS đã được đóng", + "ToastSendEbookToDeviceFailed": "Gửi ebook đến thiết bị thất bại", + "ToastSendEbookToDeviceSuccess": "Ebook đã được gửi đến thiết bị \"{0}\"", + "ToastSeriesUpdateFailed": "Cập nhật loạt truyện thất bại", + "ToastSeriesUpdateSuccess": "Cập nhật loạt truyện thành công", + "ToastSessionDeleteFailed": "Xóa phiên thất bại", + "ToastSessionDeleteSuccess": "Phiên đã được xóa", + "ToastSocketConnected": "Kết nối socket", + "ToastSocketDisconnected": "Ngắt kết nối socket", + "ToastSocketFailedToConnect": "Không thể kết nối socket", + "ToastUserDeleteFailed": "Xóa người dùng thất bại", + "ToastUserDeleteSuccess": "Người dùng đã được xóa" +} From 450fa45360f21e586968886ee7b6996caf8e7eef Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 9 Mar 2024 20:09:08 -0600 Subject: [PATCH 0425/2145] Update Vietnamese datefns locale code --- client/plugins/i18n.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 4a88d6f6..504db248 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -22,7 +22,7 @@ const languageCodeMap = { 'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' }, 'ru': { label: 'Русский', dateFnsLocale: 'ru' }, 'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, - 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'viVN' }, + 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, } Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { From 727dad7e195b22e182789dc55f72ce9433471da2 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 10 Mar 2024 09:43:24 -0500 Subject: [PATCH 0426/2145] Update multi select highlight color to yellow, remove console logs --- client/components/ui/MultiSelect.vue | 5 +---- client/components/ui/MultiSelectQueryInput.vue | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue index 26c85bbd..516b062d 100644 --- a/client/components/ui/MultiSelect.vue +++ b/client/components/ui/MultiSelect.vue @@ -17,7 +17,7 @@ <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 itemsToShow"> - <li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-sky-400' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent> + <li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-yellow-300' : ''" 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 }}</span> </div> @@ -129,14 +129,12 @@ export default { } else { this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1) } - console.log('ArrowDown. this.selectedMenuItemIndex=', this.selectedMenuItemIndex) } else if (event.key === 'ArrowUp') { if (this.selectedMenuItemIndex === null) { this.selectedMenuItemIndex = items.length - 1 } else { this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0) } - console.log('ArrowUp. this.selectedMenuItemIndex=', this.selectedMenuItemIndex) } this.recalcScroll() return @@ -144,7 +142,6 @@ export default { if (this.selectedMenuItemIndex !== null) { this.clickedOption(event, items[this.selectedMenuItemIndex]) } else { - console.log('Enter. this.textInput=', this.textInput) this.submitForm() } return diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue index d671e29d..6e9c0f10 100644 --- a/client/components/ui/MultiSelectQueryInput.vue +++ b/client/components/ui/MultiSelectQueryInput.vue @@ -20,7 +20,7 @@ <ul ref="menu" v-show="showMenu" class="absolute z-60 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 itemsToShow"> - <li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-sky-400' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent> + <li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-yellow-300' : ''" 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.name }}</span> </div> @@ -143,7 +143,7 @@ export default { } this.recalcScroll() return - } else if (event.key === 'Enter') { + } else if (event.key === 'Enter') { if (this.selectedMenuItemIndex !== null) { this.clickedOption(event, items[this.selectedMenuItemIndex]) } else { @@ -155,7 +155,7 @@ export default { clearTimeout(this.typingTimeout) this.typingTimeout = setTimeout(() => { this.search() - }, 250) + }, 250) this.setInputWidth() }, setInputWidth() { From 76a1f48c62b8626e855794f44aed766cb8d44b7e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 11 Mar 2024 11:11:13 -0500 Subject: [PATCH 0427/2145] Remove UID/GID from Server constructor --- index.js | 4 +--- prod.js | 4 +--- server/Server.js | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 585af22e..79d06c3d 100644 --- a/index.js +++ b/index.js @@ -18,12 +18,10 @@ const PORT = process.env.PORT || 80 const HOST = process.env.HOST const CONFIG_PATH = process.env.CONFIG_PATH || '/config' const METADATA_PATH = process.env.METADATA_PATH || '/metadata' -const UID = process.env.AUDIOBOOKSHELF_UID -const GID = process.env.AUDIOBOOKSHELF_GID const SOURCE = process.env.SOURCE || 'docker' const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '' console.log('Config', CONFIG_PATH, METADATA_PATH) -const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) +const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) Server.start() diff --git a/prod.js b/prod.js index ca54bfab..70633d5b 100644 --- a/prod.js +++ b/prod.js @@ -23,13 +23,11 @@ const PORT = options.port || process.env.PORT || 3333 const HOST = options.host || process.env.HOST const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config') const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata') -const UID = process.env.AUDIOBOOKSHELF_UID -const GID = process.env.AUDIOBOOKSHELF_GID const SOURCE = options.source || process.env.SOURCE || 'debian' const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '' console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH) -const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) +const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) Server.start() diff --git a/server/Server.js b/server/Server.js index 01b1af12..5704814d 100644 --- a/server/Server.js +++ b/server/Server.js @@ -41,13 +41,11 @@ const passport = require('passport') const expressSession = require('express-session') class Server { - constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { + constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { this.Port = PORT this.Host = HOST global.Source = SOURCE global.isWin = process.platform === 'win32' - global.Uid = isNaN(UID) ? undefined : Number(UID) - global.Gid = isNaN(GID) ? undefined : Number(GID) global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH)) global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH)) global.RouterBasePath = ROUTER_BASE_PATH From c14f9accafcc688d178f66c795826d6b8ea3b159 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 11 Mar 2024 17:07:03 -0500 Subject: [PATCH 0428/2145] Update functions for #2724 and add jsdocs --- server/Database.js | 28 +++++++++++++++++++++------- server/scanner/BookScanner.js | 24 ++++++++++++------------ 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/server/Database.js b/server/Database.js index 78d6bc7d..e3fabe1f 100644 --- a/server/Database.js +++ b/server/Database.js @@ -689,19 +689,33 @@ class Database { return this.libraryFilterData[libraryId].series.some(se => se.id === seriesId) } - async getAuthorByName(libraryId, authorName) { + /** + * Get author id for library by name. Uses library filter data if available + * + * @param {string} libraryId + * @param {string} authorName + * @returns {Promise<string>} author id or null if not found + */ + async getAuthorIdByName(libraryId, authorName) { if (!this.libraryFilterData[libraryId]) { - return await this.authorModel.getOldByNameAndLibrary(authorName, libraryId) + return (await this.authorModel.getOldByNameAndLibrary(authorName, libraryId))?.id || null } - return this.libraryFilterData[libraryId].authors.find(au => au.name === authorName) + return this.libraryFilterData[libraryId].authors.find(au => au.name === authorName)?.id || null } - async getSeriesByName(libraryId, seriesName) { + /** + * Get series id for library by name. Uses library filter data if available + * + * @param {string} libraryId + * @param {string} seriesName + * @returns {Promise<string>} series id or null if not found + */ + async getSeriesIdByName(libraryId, seriesName) { if (!this.libraryFilterData[libraryId]) { - return await this.seriesModel.getOldByNameAndLibrary(seriesName, libraryId) + return (await this.seriesModel.getOldByNameAndLibrary(seriesName, libraryId))?.id || null } - return this.libraryFilterData[libraryId].series.find(se => se.name === seriesName) - } + return this.libraryFilterData[libraryId].series.find(se => se.name === seriesName)?.id || null + } /** * Reset numIssues for library diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index fbe3ccfb..c738c52e 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -186,11 +186,11 @@ class BookScanner { // Check for authors added for (const authorName of bookMetadata.authors) { if (!media.authors.some(au => au.name === authorName)) { - const existingAuthor = await Database.getAuthorByName(libraryItemData.libraryId, authorName) - if (existingAuthor) { + const existingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName) + if (existingAuthorId) { await Database.bookAuthorModel.create({ bookId: media.id, - authorId: existingAuthor.id + authorId: existingAuthorId }) libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added author "${authorName}"`) authorsUpdated = true @@ -221,11 +221,11 @@ class BookScanner { for (const seriesObj of bookMetadata.series) { const existingBookSeries = media.series.find(se => se.name === seriesObj.name) if (!existingBookSeries) { - const existingSeries = await Database.getSeriesByName(libraryItemData.libraryId, seriesObj.name) - if (existingSeries) { + const existingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name) + if (existingSeriesId) { await Database.bookSeriesModel.create({ bookId: media.id, - seriesId: existingSeries.id, + seriesId: existingSeriesId, sequence: seriesObj.sequence }) libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added series "${seriesObj.name}"${seriesObj.sequence ? ` with sequence "${seriesObj.sequence}"` : ''}`) @@ -443,10 +443,10 @@ class BookScanner { } if (bookMetadata.authors.length) { for (const authorName of bookMetadata.authors) { - const matchingAuthor = await Database.getAuthorByName(libraryItemData.libraryId, authorName) - if (matchingAuthor) { + const matchingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName) + if (matchingAuthorId) { bookObject.bookAuthors.push({ - authorId: matchingAuthor.id + authorId: matchingAuthorId }) } else { // New author @@ -463,10 +463,10 @@ class BookScanner { if (bookMetadata.series.length) { for (const seriesObj of bookMetadata.series) { if (!seriesObj.name) continue - const matchingSeries = await Database.getSeriesByName(libraryItemData.libraryId, seriesObj.name) - if (matchingSeries) { + const matchingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name) + if (matchingSeriesId) { bookObject.bookSeries.push({ - seriesId: matchingSeries.id, + seriesId: matchingSeriesId, sequence: seriesObj.sequence }) } else { From 7b1b44879585da6d1fcf3276292feca74cdc749c Mon Sep 17 00:00:00 2001 From: -Shiken- <58270741+den13501@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:29:24 +0800 Subject: [PATCH 0429/2145] Add traditional Chinese translation for traditional user. --- client/plugins/i18n.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 504db248..246ffebd 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -24,6 +24,7 @@ const languageCodeMap = { 'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, + 'zh-tw': { label: '正體中文 (Traditional Chinese)', dateFnsLocale: 'zhTW' }, } Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { return { From d840905a97bdc610104969efab2180a090484e10 Mon Sep 17 00:00:00 2001 From: -Shiken- <58270741+den13501@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:36:42 +0800 Subject: [PATCH 0430/2145] Create zh-tw.json add traditional Chinese translation text. --- client/strings/zh-tw.json | 779 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 779 insertions(+) create mode 100644 client/strings/zh-tw.json diff --git a/client/strings/zh-tw.json b/client/strings/zh-tw.json new file mode 100644 index 00000000..64715680 --- /dev/null +++ b/client/strings/zh-tw.json @@ -0,0 +1,779 @@ +{ + "ButtonAdd": "增加", + "ButtonAddChapters": "新增章節", + "ButtonAddDevice": "新增設備", + "ButtonAddLibrary": "新增庫", + "ButtonAddPodcasts": "新增播客", + "ButtonAddUser": "新增使用者", + "ButtonAddYourFirstLibrary": "新增第一個媒體庫", + "ButtonApply": "應用", + "ButtonApplyChapters": "應用到章節", + "ButtonAuthors": "作者", + "ButtonBrowseForFolder": "瀏覽資料夾", + "ButtonCancel": "取消", + "ButtonCancelEncode": "取消編碼", + "ButtonChangeRootPassword": "更改 Root 密碼", + "ButtonCheckAndDownloadNewEpisodes": "檢查並下載新劇集", + "ButtonChooseAFolder": "選擇資料夾", + "ButtonChooseFiles": "選擇檔案", + "ButtonClearFilter": "清除過濾器", + "ButtonCloseFeed": "關閉源", + "ButtonCollections": "收藏", + "ButtonConfigureScanner": "配置掃描", + "ButtonCreate": "創建", + "ButtonCreateBackup": "創建備份", + "ButtonDelete": "刪除", + "ButtonDownloadQueue": "下載佇列, + "ButtonEdit": "編輯", + "ButtonEditChapters": "編輯章節", + "ButtonEditPodcast": "編輯播客", + "ButtonForceReScan": "強制重新掃描", + "ButtonFullPath": "完整路徑", + "ButtonHide": "隱藏", + "ButtonHome": "首頁", + "ButtonIssues": "問題", + "ButtonJumpBackward": "Jump Backward", + "ButtonJumpForward": "Jump Forward", + "ButtonLatest": "最新", + "ButtonLibrary": "媒體庫", + "ButtonLogout": "登出", + "ButtonLookup": "查找", + "ButtonManageTracks": "管理音軌", + "ButtonMapChapterTitles": "章節標題結構", + "ButtonMatchAllAuthors": "匹配所有作者", + "ButtonMatchBooks": "匹配圖書", + "ButtonNevermind": "沒關係", + "ButtonNext": "下個", + "ButtonNextChapter": "下個章節", + "ButtonOk": "確定", + "ButtonOpenFeed": "打開源", + "ButtonOpenManager": "打開管理器", + "ButtonPause": "暫停", + "ButtonPlay": "播放", + "ButtonPlaying": "正在播放", + "ButtonPlaylists": "播放列表", + "ButtonPrevious": "Previous", + "ButtonPreviousChapter": "過去的章節", + "ButtonPurgeAllCache": "清理所有快取", + "ButtonPurgeItemsCache": "清理項目快取", + "ButtonPurgeMediaProgress": "清理媒體進度", + "ButtonQueueAddItem": "新增到佇列", + "ButtonQueueRemoveItem": "從佇列中移除", + "ButtonQuickMatch": "快速匹配", + "ButtonRead": "讀取", + "ButtonRefresh": "重整", + "ButtonRemove": "移除", + "ButtonRemoveAll": "移除所有", + "ButtonRemoveAllLibraryItems": "移除所有媒體庫項目", + "ButtonRemoveFromContinueListening": "從繼續收聽中刪除", + "ButtonRemoveFromContinueReading": "從繼續閱讀中刪除", + "ButtonRemoveSeriesFromContinueSeries": "從繼續收聽系列中刪除", + "ButtonReScan": "重新掃描", + "ButtonReset": "重置", + "ButtonResetToDefault": "重置為預設", + "ButtonRestore": "恢復", + "ButtonSave": "保存", + "ButtonSaveAndClose": "保存並關閉", + "ButtonSaveTracklist": "保存音軌列表", + "ButtonScan": "掃描", + "ButtonScanLibrary": "掃描庫", + "ButtonSearch": "查找", + "ButtonSelectFolderPath": "選擇資料夾路徑", + "ButtonSeries": "系列", + "ButtonSetChaptersFromTracks": "將音軌設定為章節", + "ButtonShare": "Share", + "ButtonShiftTimes": "快速調整時間", + "ButtonShow": "顯示", + "ButtonStartM4BEncode": "開始 M4B 編碼", + "ButtonStartMetadataEmbed": "開始嵌入元數據", + "ButtonSubmit": "提交", + "ButtonTest": "測試", + "ButtonUpload": "上傳", + "ButtonUploadBackup": "上傳備份", + "ButtonUploadCover": "上傳封面", + "ButtonUploadOPMLFile": "上傳 OPML 檔", + "ButtonUserDelete": "刪除使用者 {0}", + "ButtonUserEdit": "編輯使用者 {0}", + "ButtonViewAll": "查看全部", + "ButtonYes": "確定", + "ErrorUploadFetchMetadataAPI": "獲取元數據時出錯", + "ErrorUploadFetchMetadataNoResults": "無法獲取元數據 - 嘗試更新標題和/或作者", + "ErrorUploadLacksTitle": "必須有標題", + "HeaderAccount": "帳號", + "HeaderAdvanced": "高級", + "HeaderAppriseNotificationSettings": "測試通知設定", + "HeaderAudiobookTools": "有聲書檔案管理工具", + "HeaderAudioTracks": "音軌", + "HeaderAuthentication": "身份驗證", + "HeaderBackups": "備份", + "HeaderChangePassword": "更改密碼", + "HeaderChapters": "章節", + "HeaderChooseAFolder": "選擇資料夾", + "HeaderCollection": "收藏", + "HeaderCollectionItems": "收藏項目", + "HeaderCover": "封面", + "HeaderCurrentDownloads": "當前下載", + "HeaderCustomMetadataProviders": "自訂 Metadata 提供者", + "HeaderDetails": "詳情", + "HeaderDownloadQueue": "下載佇列", + "HeaderEbookFiles": "電子書檔", + "HeaderEmail": "郵箱", + "HeaderEmailSettings": "郵箱設定", + "HeaderEpisodes": "劇集", + "HeaderEreaderDevices": "Ereader 設備", + "HeaderEreaderSettings": "Ereader 設定", + "HeaderFiles": "檔案", + "HeaderFindChapters": "查找章節", + "HeaderIgnoredFiles": "忽略的檔案", + "HeaderItemFiles": "項目檔案", + "HeaderItemMetadataUtils": "項目元數據管理", + "HeaderLastListeningSession": "最後一次收聽會話", + "HeaderLatestEpisodes": "最新劇集", + "HeaderLibraries": "媒體庫", + "HeaderLibraryFiles": "媒體庫檔案", + "HeaderLibraryStats": "媒體庫統計數據", + "HeaderListeningSessions": "收聽會話", + "HeaderListeningStats": "收聽統計數據", + "HeaderLogin": "登入", + "HeaderLogs": "日誌", + "HeaderManageGenres": "管理流派", + "HeaderManageTags": "管理標籤", + "HeaderMapDetails": "編輯詳情", + "HeaderMatch": "匹配", + "HeaderMetadataOrderOfPrecedence": "元數據優先級", + "HeaderMetadataToEmbed": "嵌入元數據", + "HeaderNewAccount": "新建帳號", + "HeaderNewLibrary": "新建媒體庫", + "HeaderNotifications": "通知", + "HeaderOpenIDConnectAuthentication": "OpenID 連接身份驗證", + "HeaderOpenRSSFeed": "打開 RSS 源", + "HeaderOtherFiles": "其他檔案", + "HeaderPasswordAuthentication": "密碼認證", + "HeaderPermissions": "權限", + "HeaderPlayerQueue": "播放佇列", + "HeaderPlaylist": "播放列表", + "HeaderPlaylistItems": "播放列表項目", + "HeaderPodcastsToAdd": "要新增的播客", + "HeaderPreviewCover": "預覽封面", + "HeaderRemoveEpisode": "移除劇集", + "HeaderRemoveEpisodes": "移除 {0} 劇集", + "HeaderRSSFeedGeneral": "RSS 詳細信息", + "HeaderRSSFeedIsOpen": "RSS 源已打開", + "HeaderRSSFeeds": "RSS 訂閱", + "HeaderSavedMediaProgress": "保存媒體進度", + "HeaderSchedule": "計劃任務", + "HeaderScheduleLibraryScans": "自動掃描媒體庫", + "HeaderSession": "會話", + "HeaderSetBackupSchedule": "設定備份計劃任務", + "HeaderSettings": "設定", + "HeaderSettingsDisplay": "顯示", + "HeaderSettingsExperimental": "實驗功能", + "HeaderSettingsGeneral": "通用", + "HeaderSettingsScanner": "掃描", + "HeaderSleepTimer": "睡眠計時", + "HeaderStatsLargestItems": "最大的項目", + "HeaderStatsLongestItems": "項目時長(小時)", + "HeaderStatsMinutesListeningChart": "收聽分鐘數(最近7天)", + "HeaderStatsRecentSessions": "歷史會話", + "HeaderStatsTop10Authors": "前 10 位作者", + "HeaderStatsTop5Genres": "前 5 種流派", + "HeaderTableOfContents": "目錄", + "HeaderTools": "工具", + "HeaderUpdateAccount": "更新帳號", + "HeaderUpdateAuthor": "更新作者", + "HeaderUpdateDetails": "更新詳情", + "HeaderUpdateLibrary": "更新媒體庫", + "HeaderUsers": "使用者", + "HeaderYearReview": "Year {0} in Review", + "HeaderYourStats": "你的統計數據", + "LabelAbridged": "概要", + "LabelAccountType": "帳號類型", + "LabelAccountTypeAdmin": "管理員", + "LabelAccountTypeGuest": "來賓", + "LabelAccountTypeUser": "使用者", + "LabelActivity": "活動", + "LabelAdded": "新增", + "LabelAddedAt": "新增於", + "LabelAddToCollection": "新增到收藏", + "LabelAddToCollectionBatch": "批量新增 {0} 個媒體到收藏", + "LabelAddToPlaylist": "新增到播放列表", + "LabelAddToPlaylistBatch": "新增 {0} 個項目到播放列表", + "LabelAdminUsersOnly": "僅限管理員使用者", + "LabelAll": "全部", + "LabelAllUsers": "所有使用者", + "LabelAllUsersExcludingGuests": "除訪客外的所有使用者", + "LabelAllUsersIncludingGuests": "包括訪客的所有使用者", + "LabelAlreadyInYourLibrary": "已存在你的庫中", + "LabelAppend": "附加", + "LabelAuthor": "作者", + "LabelAuthorFirstLast": "作者 (姓 名)", + "LabelAuthorLastFirst": "作者 (名, 姓)", + "LabelAuthors": "作者", + "LabelAutoDownloadEpisodes": "自動下載劇集", + "LabelAutoFetchMetadata": "自動獲取元數據", + "LabelAutoFetchMetadataHelp": "獲取標題, 作者和系列的元數據以簡化上傳. 上傳後可能需要匹配其他元數據.", + "LabelAutoLaunch": "自動啟動", + "LabelAutoLaunchDescription": "導航到登入頁面時自動重定向到身份驗證提供程序 (手動覆蓋路徑 <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "自動註冊", + "LabelAutoRegisterDescription": "登入後自動創建新使用者", + "LabelBackToUser": "返回到使用者", + "LabelBackupLocation": "備份位置", + "LabelBackupsEnableAutomaticBackups": "啟用自動備份", + "LabelBackupsEnableAutomaticBackupsHelp": "備份保存到 /metadata/backups", + "LabelBackupsMaxBackupSize": "最大備份大小 (GB)", + "LabelBackupsMaxBackupSizeHelp": "為了防止錯誤配置, 如果備份超過配置的大小, 備份將失敗.", + "LabelBackupsNumberToKeep": "要保留的備份個數", + "LabelBackupsNumberToKeepHelp": "一次只能刪除一個備份, 因此如果你已經有超過此數量的備份, 則應手動刪除它們.", + "LabelBitrate": "位元率", + "LabelBooks": "圖書", + "LabelButtonText": "按鈕文本", + "LabelChangePassword": "修改密碼", + "LabelChannels": "聲道", + "LabelChapters": "章節", + "LabelChaptersFound": "找到的章節", + "LabelChapterTitle": "章節標題", + "LabelClickForMoreInfo": "點擊了解更多資訊", + "LabelClosePlayer": "關閉播放器", + "LabelCodec": "編解碼", + "LabelCollapseSeries": "折疊系列", + "LabelCollection": "收藏", + "LabelCollections": "收藏", + "LabelComplete": "已完成", + "LabelConfirmPassword": "確認密碼", + "LabelContinueListening": "繼續收聽", + "LabelContinueReading": "繼續閱讀", + "LabelContinueSeries": "繼續收聽系列", + "LabelCover": "封面", + "LabelCoverImageURL": "封面圖像 URL", + "LabelCreatedAt": "創建時間", + "LabelCronExpression": "計劃任務表達式", + "LabelCurrent": "當前", + "LabelCurrently": "當前:", + "LabelCustomCronExpression": "自定義計劃任務表達式:", + "LabelDatetime": "日期時間", + "LabelDeleteFromFileSystemCheckbox": "從檔案系統刪除 (取消選中僅從資料庫中刪除)", + "LabelDescription": "描述", + "LabelDeselectAll": "全部取消選擇", + "LabelDevice": "設備", + "LabelDeviceInfo": "設備資訊", + "LabelDeviceIsAvailableTo": "設備可用於...", + "LabelDirectory": "目錄", + "LabelDiscFromFilename": "從檔名獲取光碟", + "LabelDiscFromMetadata": "從元數據獲取光碟", + "LabelDiscover": "發現", + "LabelDownload": "下載", + "LabelDownloadNEpisodes": "下載 {0} 集", + "LabelDuration": "持續時間", + "LabelDurationFound": "找到持續時間:", + "LabelEbook": "電子書", + "LabelEbooks": "電子書", + "LabelEdit": "編輯", + "LabelEmail": "郵箱", + "LabelEmailSettingsFromAddress": "發件人位址", + "LabelEmailSettingsSecure": "安全", + "LabelEmailSettingsSecureHelp": "如果選是, 則連接將在連接到伺服器時使用TLS. 如果選否, 則若伺服器支援STARTTLS擴展, 則使用TLS. 在大多數情況下, 如果連接到465埠, 請將該值設定為是. 對於587或25埠, 請保持為否. (來自nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "測試位址", + "LabelEmbeddedCover": "嵌入封面", + "LabelEnable": "啟用", + "LabelEnd": "結束", + "LabelEpisode": "劇集", + "LabelEpisodeTitle": "劇集標題", + "LabelEpisodeType": "劇集類型", + "LabelExample": "示例", + "LabelExplicit": "信息準確", + "LabelFeedURL": "源 URL", + "LabelFetchingMetadata": "正在獲取元數據", + "LabelFile": "檔案", + "LabelFileBirthtime": "檔案創建時間", + "LabelFileModified": "檔案修改時間", + "LabelFilename": "檔名", + "LabelFilterByUser": "按使用者篩選", + "LabelFindEpisodes": "查找劇集", + "LabelFinished": "已聽完", + "LabelFolder": "資料夾", + "LabelFolders": "資料夾", + "LabelFontBold": "Bold", + "LabelFontFamily": "字體系列", + "LabelFontItalic": "斜體", + "LabelFontScale": "字體比例", + "LabelFontStrikethrough": "刪除線", + "LabelFormat": "編碼格式", + "LabelGenre": "流派", + "LabelGenres": "流派", + "LabelHardDeleteFile": "完全刪除檔案", + "LabelHasEbook": "有電子書", + "LabelHasSupplementaryEbook": "有補充電子書", + "LabelHighestPriority": "最高優先級", + "LabelHost": "主機", + "LabelHour": "小時", + "LabelIcon": "圖標", + "LabelImageURLFromTheWeb": "來自 Web 圖像的 URL", + "LabelIncludeInTracklist": "包含在音軌列表中", + "LabelIncomplete": "未聽完", + "LabelInProgress": "正在聽", + "LabelInterval": "間隔", + "LabelIntervalCustomDailyWeekly": "自定義 每天 / 每周", + "LabelIntervalEvery12Hours": "每 12 小時", + "LabelIntervalEvery15Minutes": "每 15 分鐘", + "LabelIntervalEvery2Hours": "每 2 小時", + "LabelIntervalEvery30Minutes": "每 30 分鐘", + "LabelIntervalEvery6Hours": "每 6 小時", + "LabelIntervalEveryDay": "每天", + "LabelIntervalEveryHour": "每小時", + "LabelInvalidParts": "無效部件", + "LabelInvert": "倒轉", + "LabelItem": "項目", + "LabelLanguage": "語言", + "LabelLanguageDefaultServer": "預設伺服器語言", + "LabelLastBookAdded": "最後新增的書", + "LabelLastBookUpdated": "最後更新的書", + "LabelLastSeen": "上次查看時間", + "LabelLastTime": "最近一次", + "LabelLastUpdate": "最近更新", + "LabelLayout": "布局", + "LabelLayoutSinglePage": "單頁", + "LabelLayoutSplitPage": "分頁", + "LabelLess": "較少", + "LabelLibrariesAccessibleToUser": "使用者可存取的媒體庫", + "LabelLibrary": "媒體庫", + "LabelLibraryItem": "媒體庫項目", + "LabelLibraryName": "媒體庫名稱", + "LabelLimit": "限制", + "LabelLineSpacing": "行間距", + "LabelListenAgain": "再次收聽", + "LabelLogLevelDebug": "調試", + "LabelLogLevelInfo": "信息", + "LabelLogLevelWarn": "警告", + "LabelLookForNewEpisodesAfterDate": "在此日期後查找新劇集", + "LabelLowestPriority": "最低優先級", + "LabelMatchExistingUsersBy": "匹配現有使用者", + "LabelMatchExistingUsersByDescription": "用於連接現有使用者. 連接後, 使用者將通過SSO提供商提供的唯一 id 進行匹配", + "LabelMediaPlayer": "媒體播放器", + "LabelMediaType": "媒體類型", + "LabelMetadataOrderOfPrecedenceDescription": "較高優先級的元數據源將覆蓋較低優先級的元數據源", + "LabelMetadataProvider": "元數據提供者", + "LabelMetaTag": "元數據標籤", + "LabelMetaTags": "元標籤", + "LabelMinute": "分鐘", + "LabelMissing": "丟失", + "LabelMissingEbook": "Has no ebook", + "LabelMissingParts": "丟失的部分", + "LabelMissingSupplementaryEbook": "Has no supplementary ebook", + "LabelMobileRedirectURIs": "允許移動應用重定向 URI", + "LabelMobileRedirectURIsDescription": "這是移動應用程序的有效重定向 URI 白名單. 預設值為 <code>audiobookshelf://oauth</code>,您可以刪除它或加入其他 URI 以進行第三方應用集成. 使用星號 (<code>*</code>) 作為唯一條目允許任何 URI.", + "LabelMore": "更多", + "LabelMoreInfo": "更多..", + "LabelName": "名稱", + "LabelNarrator": "講述者", + "LabelNarrators": "講述者", + "LabelNew": "新建", + "LabelNewestAuthors": "最新作者", + "LabelNewestEpisodes": "最新劇集", + "LabelNewPassword": "新密碼", + "LabelNextBackupDate": "下次備份日期", + "LabelNextScheduledRun": "下次任務運行", + "LabelNoEpisodesSelected": "未選擇任何劇集", + "LabelNotes": "注釋", + "LabelNotFinished": "未聽完", + "LabelNotificationAppriseURL": "通知 URL(s)", + "LabelNotificationAvailableVariables": "可用變量", + "LabelNotificationBodyTemplate": "正文模板", + "LabelNotificationEvent": "通知事件", + "LabelNotificationsMaxFailedAttempts": "最大失敗嘗試次數", + "LabelNotificationsMaxFailedAttemptsHelp": "如果多次發送失敗,通知將被禁用", + "LabelNotificationsMaxQueueSize": "通知事件的最大佇列大小", + "LabelNotificationsMaxQueueSizeHelp": "通知事件被限制為每秒觸發 1 個. 如果佇列處於最大大小, 則將忽略事件. 這可以防止通知垃圾郵件.", + "LabelNotificationTitleTemplate": "標題模板", + "LabelNotStarted": "未開始", + "LabelNumberOfBooks": "圖書數量", + "LabelNumberOfEpisodes": "# 集", + "LabelOpenRSSFeed": "打開 RSS 源", + "LabelOverwrite": "覆蓋", + "LabelPassword": "密碼", + "LabelPath": "路徑", + "LabelPermissionsAccessAllLibraries": "可以存取所有媒體庫", + "LabelPermissionsAccessAllTags": "可以存取所有標籤", + "LabelPermissionsAccessExplicitContent": "可以存取顯式內容", + "LabelPermissionsDelete": "可以刪除", + "LabelPermissionsDownload": "可以下載", + "LabelPermissionsUpdate": "可以更新", + "LabelPermissionsUpload": "可以上傳", + "LabelPersonalYearReview": "你的年度回顧 ({0})", + "LabelPhotoPathURL": "圖片路徑或 URL", + "LabelPlaylists": "播放列表", + "LabelPlayMethod": "播放方法", + "LabelPodcast": "播客", + "LabelPodcasts": "播客", + "LabelPodcastSearchRegion": "播客搜尋地區", + "LabelPodcastType": "播客類型", + "LabelPort": "埠", + "LabelPrefixesToIgnore": "忽略的前綴 (不區分大小寫)", + "LabelPreventIndexing": "防止 iTunes 和 Google 播客目錄對你的源進行索引", + "LabelPrimaryEbook": "主電子書", + "LabelProgress": "進度", + "LabelProvider": "供應商", + "LabelPubDate": "出版日期", + "LabelPublisher": "出版商", + "LabelPublishYear": "發布年份", + "LabelRead": "閱讀", + "LabelReadAgain": "再次閱讀", + "LabelReadEbookWithoutProgress": "閱讀電子書而不保存進度", + "LabelRecentlyAdded": "最近新增", + "LabelRecentSeries": "最近新增系列", + "LabelRecommended": "推薦內容", + "LabelRedo": "重做", + "LabelRegion": "區域", + "LabelReleaseDate": "發布日期", + "LabelRemoveCover": "移除封面", + "LabelRowsPerPage": "每頁行數", + "LabelRSSFeedCustomOwnerEmail": "自定義所有者電子郵件", + "LabelRSSFeedCustomOwnerName": "自定義所有者名稱", + "LabelRSSFeedOpen": "打開 RSS 源", + "LabelRSSFeedPreventIndexing": "防止索引", + "LabelRSSFeedSlug": "RSS 源段", + "LabelRSSFeedURL": "RSS 源 URL", + "LabelSearchTerm": "搜尋項", + "LabelSearchTitle": "搜尋標題", + "LabelSearchTitleOrASIN": "搜尋標題或 ASIN", + "LabelSeason": "季", + "LabelSelectAllEpisodes": "選擇所有劇集", + "LabelSelectEpisodesShowing": "選擇正在播放的 {0} 劇集", + "LabelSelectUsers": "Select users", + "LabelSendEbookToDevice": "發送電子書到...", + "LabelSequence": "序列", + "LabelSeries": "系列", + "LabelSeriesName": "系列名稱", + "LabelSeriesProgress": "系列進度", + "LabelServerYearReview": "伺服器年度回顧 ({0})", + "LabelSetEbookAsPrimary": "設定為主", + "LabelSetEbookAsSupplementary": "設定為補充", + "LabelSettingsAudiobooksOnly": "僅有聲書", + "LabelSettingsAudiobooksOnlyHelp": "啟用此設定將忽略電子書檔, 除非它們位於有聲書資料夾中, 在這種情況下, 它們將被設定為補充電子書", + "LabelSettingsBookshelfViewHelp": "帶有木架子的擬物化設計", + "LabelSettingsChromecastSupport": "Chromecast 支援", + "LabelSettingsDateFormat": "日期格式", + "LabelSettingsDisableWatcher": "禁用監視程序", + "LabelSettingsDisableWatcherForLibrary": "禁用媒體庫的資料夾監視程序", + "LabelSettingsDisableWatcherHelp": "檢測到檔案更改時禁用自動新增和更新項目. *需要重啟伺服器", + "LabelSettingsEnableWatcher": "啟用監視程序", + "LabelSettingsEnableWatcherForLibrary": "為庫啟用資料夾監視程序", + "LabelSettingsEnableWatcherHelp": "當檢測到檔案更改時, 啟用項目的自動新增/更新. *需要重新啟動伺服器", + "LabelSettingsExperimentalFeatures": "實驗功能", + "LabelSettingsExperimentalFeaturesHelp": "開發中的功能需要你的反饋並幫助測試. 點擊打開 github 討論.", + "LabelSettingsFindCovers": "查找封面", + "LabelSettingsFindCoversHelp": "如果你的有聲書在資料夾中沒有嵌入封面或封面圖像, 掃描將嘗試查找封面.<br>注意: 這將延長掃描時間", + "LabelSettingsHideSingleBookSeries": "隱藏單書系列", + "LabelSettingsHideSingleBookSeriesHelp": "只有一本書的系列將從系列頁面和主頁書架中隱藏.", + "LabelSettingsHomePageBookshelfView": "首頁使用書架視圖", + "LabelSettingsLibraryBookshelfView": "媒體庫使用書架視圖", + "LabelSettingsParseSubtitles": "解析副標題", + "LabelSettingsParseSubtitlesHelp": "從有聲書資料夾中提取副標題.<br>副標題必須用 \" - \" 分隔.<br>例: \"書名 - 這裡是副標題\" 則顯示副標題 \"這裡是副標題\"", + "LabelSettingsPreferMatchedMetadata": "首選匹配的元數據", + "LabelSettingsPreferMatchedMetadataHelp": "使用快速匹配時, 匹配的數據將覆蓋項目詳細信息. 預設情況下, 快速匹配將只填充缺少的詳細信息.", + "LabelSettingsSkipMatchingBooksWithASIN": "跳過匹配已有 ASIN 的圖書", + "LabelSettingsSkipMatchingBooksWithISBN": "跳過匹配已有 ISBN 的圖書", + "LabelSettingsSortingIgnorePrefixes": "排序時忽略前綴", + "LabelSettingsSortingIgnorePrefixesHelp": "例如: 前綴為 \"The\" 的圖書標題 \"The Book Title\" 將按 \"Book Title, The\" 進行排序", + "LabelSettingsSquareBookCovers": "使用者方形圖書封面", + "LabelSettingsSquareBookCoversHelp": "比起標準的 1.6:1 圖書封面,更喜歡使用方形封面", + "LabelSettingsStoreCoversWithItem": "存儲項目封面", + "LabelSettingsStoreCoversWithItemHelp": "預設情況下封面存儲在/metadata/items資料夾中, 啟用此設定將存儲封面在你媒體項目資料夾中. 只保留一個名為 \"cover\" 的檔案", + "LabelSettingsStoreMetadataWithItem": "存儲項目元數據", + "LabelSettingsStoreMetadataWithItemHelp": "預設情況下元數據檔案存儲在/metadata/items資料夾中, 啟用此設定將存儲元數據在你媒體項目資料夾中", + "LabelSettingsTimeFormat": "時間格式", + "LabelShowAll": "全部顯示", + "LabelSize": "檔案大小", + "LabelSleepTimer": "睡眠定時", + "LabelSlug": "Slug", + "LabelStart": "開始", + "LabelStarted": "開始於", + "LabelStartedAt": "從這開始", + "LabelStartTime": "開始時間", + "LabelStatsAudioTracks": "音軌", + "LabelStatsAuthors": "作者", + "LabelStatsBestDay": "最好的一天", + "LabelStatsDailyAverage": "每日平均值", + "LabelStatsDays": "天", + "LabelStatsDaysListened": "收聽天數", + "LabelStatsHours": "小時", + "LabelStatsInARow": "在一行", + "LabelStatsItemsFinished": "已完成的項目", + "LabelStatsItemsInLibrary": "媒體庫中的項目", + "LabelStatsMinutes": "分鐘", + "LabelStatsMinutesListening": "收聽分鐘數", + "LabelStatsOverallDays": "總計天數", + "LabelStatsOverallHours": "總計小時", + "LabelStatsWeekListening": "每周收聽", + "LabelSubtitle": "副標題", + "LabelSupportedFileTypes": "支援的檔案類型", + "LabelTag": "標籤", + "LabelTags": "標籤", + "LabelTagsAccessibleToUser": "使用者可存取的標籤", + "LabelTagsNotAccessibleToUser": "使用者無法存取標籤", + "LabelTasks": "正在運行的任務", + "LabelTextEditorBulletedList": "項目符號列表", + "LabelTextEditorLink": "Link", + "LabelTextEditorNumberedList": "編號列表", + "LabelTextEditorUnlink": "取消連結", + "LabelTheme": "主題", + "LabelThemeDark": "黑暗", + "LabelThemeLight": "明亮", + "LabelTimeBase": "時間基準", + "LabelTimeListened": "收聽時間", + "LabelTimeListenedToday": "今日收聽的時間", + "LabelTimeRemaining": "剩餘 {0}", + "LabelTimeToShift": "快速調整時間以秒為單位", + "LabelTitle": "標題", + "LabelToolsEmbedMetadata": "嵌入元數據", + "LabelToolsEmbedMetadataDescription": "將元數據嵌入音頻檔案, 包括封面圖像和章節.", + "LabelToolsMakeM4b": "制作 M4B 有聲書檔案", + "LabelToolsMakeM4bDescription": "生成帶有嵌入元數據, 封面圖像和章節的 .M4B 有聲書檔.", + "LabelToolsSplitM4b": "將 M4B 檔拆分為 MP3 檔", + "LabelToolsSplitM4bDescription": "從 M4B 檔創建 MP3 檔, 按章節分割, 並嵌入元數據, 封面圖像和章節.", + "LabelTotalDuration": "總持續時間", + "LabelTotalTimeListened": "總收聽時間", + "LabelTrackFromFilename": "從檔案名獲取音軌", + "LabelTrackFromMetadata": "從源數據獲取音軌", + "LabelTracks": "音軌", + "LabelTracksMultiTrack": "多軌", + "LabelTracksNone": "沒有音軌", + "LabelTracksSingleTrack": "單軌", + "LabelType": "類型", + "LabelUnabridged": "未刪節", + "LabelUndo": "Undo", + "LabelUnknown": "未知", + "LabelUpdateCover": "更新封面", + "LabelUpdateCoverHelp": "找到匹配項時允許覆蓋所選書籍存在的封面", + "LabelUpdatedAt": "更新時間", + "LabelUpdateDetails": "更新詳細信息", + "LabelUpdateDetailsHelp": "找到匹配項時允許覆蓋所選書籍存在的詳細信息", + "LabelUploaderDragAndDrop": "拖放檔案或資料夾", + "LabelUploaderDropFiles": "刪除檔案", + "LabelUploaderItemFetchMetadataHelp": "自動獲取標題, 作者和系列", + "LabelUseChapterTrack": "使用章節音軌", + "LabelUseFullTrack": "使用完整音軌", + "LabelUser": "使用者", + "LabelUsername": "使用者名", + "LabelValue": "值", + "LabelVersion": "版本", + "LabelViewBookmarks": "查看書籤", + "LabelViewChapters": "查看章節", + "LabelViewQueue": "查看播放列表", + "LabelVolume": "音量", + "LabelWeekdaysToRun": "工作日運行", + "LabelYearReviewHide": "隱藏年度回顧", + "LabelYearReviewShow": "顯示年度回顧", + "LabelYourAudiobookDuration": "你的有聲書持續時間", + "LabelYourBookmarks": "你的書籤", + "LabelYourPlaylists": "你的播放列表", + "LabelYourProgress": "你的進度", + "MessageAddToPlayerQueue": "新增到播放佇列", + "MessageAppriseDescription": "要使用此功能,您需要運行一個 <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> 實例或一個可以處理這些相同請求的 API. <br />Apprise API Url 應該是發送通知的完整 URL 路徑, 例如: 如果你的 API 實例運行在 <code>http://192.168.1.1:8337</code>, 那么你可以輸入 <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "備份包括使用者, 使用者進度, 媒體庫項目詳細信息, 伺服器設定和圖像, 存儲在 <code>/metadata/items</code> & <code>/metadata/authors</code>. 備份不包括存儲在您的媒體庫資料夾中的任何檔案.", + "MessageBatchQuickMatchDescription": "快速匹配將嘗試為所選項目新增缺少的封面和元數據. 啟用以下選項以允許快速匹配覆蓋現有封面和或元數據.", + "MessageBookshelfNoCollections": "你尚未進行任何收藏", + "MessageBookshelfNoResultsForFilter": "過濾器無結果 \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "沒有打開的 RSS 源", + "MessageBookshelfNoSeries": "你沒有系列", + "MessageChapterEndIsAfter": "章節結束是在有聲書結束之後", + "MessageChapterErrorFirstNotZero": "第一章節必須從 0 開始", + "MessageChapterErrorStartGteDuration": "無效的開始時間, 必須小於有聲書持續時間", + "MessageChapterErrorStartLtPrev": "無效的開始時間, 必須大於或等於上一章節的開始時間", + "MessageChapterStartIsAfter": "章節開始是在有聲書結束之後", + "MessageCheckingCron": "檢查計劃任務...", + "MessageConfirmCloseFeed": "你確定要關閉此訂閱源嗎?", + "MessageConfirmDeleteBackup": "你確定要刪除備份 {0}?", + "MessageConfirmDeleteFile": "這將從檔案系統中刪除該檔案. 你確定嗎?", + "MessageConfirmDeleteLibrary": "你確定要永久刪除媒體庫 \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "這將從資料庫和檔案系統中刪除庫項目. 你確定嗎?", + "MessageConfirmDeleteLibraryItems": "這將從資料庫和檔案系統中刪除 {0} 個庫項目. 你確定嗎?", + "MessageConfirmDeleteSession": "你確定要刪除此會話嗎?", + "MessageConfirmForceReScan": "你確定要強制重新掃描嗎?", + "MessageConfirmMarkAllEpisodesFinished": "你確定要將所有劇集都標記為已完成嗎?", + "MessageConfirmMarkAllEpisodesNotFinished": "你確定要將所有劇集都標記為未完成嗎?", + "MessageConfirmMarkSeriesFinished": "你確定要將此系列中的所有書籍都標記為已聽完嗎?", + "MessageConfirmMarkSeriesNotFinished": "你確定要將此系列中的所有書籍都標記為未聽完嗎?", + "MessageConfirmQuickEmbed": "警告! 快速嵌入不會備份你的音頻檔案. 確保你有音頻檔案的備份. <br><br>你是否想繼續嗎?", + "MessageConfirmRemoveAllChapters": "你確定要移除所有章節嗎?", + "MessageConfirmRemoveAuthor": "你確定要刪除作者 \"{0}\"?", + "MessageConfirmRemoveCollection": "你確定要移除收藏 \"{0}\"?", + "MessageConfirmRemoveEpisode": "你確定要移除劇集 \"{0}\"?", + "MessageConfirmRemoveEpisodes": "你確定要移除 {0} 劇集?", + "MessageConfirmRemoveListeningSessions": "你確定要移除 {0} 收聽會話嗎?", + "MessageConfirmRemoveNarrator": "你確定要刪除演播者 \"{0}\"?", + "MessageConfirmRemovePlaylist": "你確定要移除播放列表 \"{0}\"?", + "MessageConfirmRenameGenre": "你確定要將所有項目流派 \"{0}\" 重命名到 \"{1}\"?", + "MessageConfirmRenameGenreMergeNote": "注意: 該流派已經存在, 因此它們將被合併.", + "MessageConfirmRenameGenreWarning": "警告! 已經存在有大小寫不同的類似流派 \"{0}\".", + "MessageConfirmRenameTag": "你確定要將所有項目標籤 \"{0}\" 重命名到 \"{1}\"?", + "MessageConfirmRenameTagMergeNote": "注意: 該標籤已經存在, 因此它們將被合併.", + "MessageConfirmRenameTagWarning": "警告! 已經存在有大小寫不同的類似標籤 \"{0}\".", + "MessageConfirmReScanLibraryItems": "你確定要重新掃描 {0} 個項目嗎?", + "MessageConfirmSendEbookToDevice": "你確定要發送 {0} 電子書 \"{1}\" 到設備 \"{2}\"?", + "MessageDownloadingEpisode": "正在下載劇集", + "MessageDragFilesIntoTrackOrder": "將檔案拖動到正確的音軌順序", + "MessageEmbedFinished": "嵌入完成!", + "MessageEpisodesQueuedForDownload": "{0} 個劇集排隊等待下載", + "MessageFeedURLWillBe": "源 URL 將改為 {0}", + "MessageFetching": "正在獲取...", + "MessageForceReScanDescription": "將像重新掃描一樣再次掃描所有檔案. 音頻檔 ID3 標籤, OPF 檔和文本檔將被掃描為新檔案.", + "MessageImportantNotice": "重要通知!", + "MessageInsertChapterBelow": "在下面插入章節", + "MessageItemsSelected": "已選定 {0} 個項目", + "MessageItemsUpdated": "已更新 {0} 個項目", + "MessageJoinUsOn": "加入我們", + "MessageListeningSessionsInTheLastYear": "去年收聽 {0} 個會話", + "MessageLoading": "讀取...", + "MessageLoadingFolders": "讀取資料夾...", + "MessageM4BFailed": "M4B 失敗!", + "MessageM4BFinished": "M4B 完成!", + "MessageMapChapterTitles": "將章節標題映射到現有的有聲書章節, 無需調整時間戳", + "MessageMarkAllEpisodesFinished": "標記所有劇集為已完成", + "MessageMarkAllEpisodesNotFinished": "標記所有劇集為未完成", + "MessageMarkAsFinished": "標記為已聽完", + "MessageMarkAsNotFinished": "標記為未聽完", + "MessageMatchBooksDescription": "嘗試將媒體庫中的圖書與所選搜尋提供商的圖書進行匹配, 並填寫空白的詳細信息和封面. 不覆蓋詳細信息.", + "MessageNoAudioTracks": "沒有音軌", + "MessageNoAuthors": "沒有作者", + "MessageNoBackups": "沒有備份", + "MessageNoBookmarks": "沒有書籤", + "MessageNoChapters": "沒有章節", + "MessageNoCollections": "沒有收藏", + "MessageNoCoversFound": "沒有找到封面", + "MessageNoDescription": "沒有描述", + "MessageNoDownloadsInProgress": "當前沒有正在進行的下載", + "MessageNoDownloadsQueued": "下載佇列無任務", + "MessageNoEpisodeMatchesFound": "沒有找到任何劇集匹配項", + "MessageNoEpisodes": "沒有劇集", + "MessageNoFoldersAvailable": "沒有可用資料夾", + "MessageNoGenres": "無流派", + "MessageNoIssues": "無問題", + "MessageNoItems": "無項目", + "MessageNoItemsFound": "未找到任何項目", + "MessageNoListeningSessions": "無收聽會話", + "MessageNoLogs": "無日誌", + "MessageNoMediaProgress": "無媒體進度", + "MessageNoNotifications": "無通知", + "MessageNoPodcastsFound": "未找到播客", + "MessageNoResults": "無結果", + "MessageNoSearchResultsFor": "沒有搜尋到結果 \"{0}\"", + "MessageNoSeries": "無系列", + "MessageNoTags": "無標籤", + "MessageNoTasksRunning": "沒有正在運行的任務", + "MessageNotYetImplemented": "尚未實施", + "MessageNoUpdateNecessary": "無需更新", + "MessageNoUpdatesWereNecessary": "無需更新", + "MessageNoUserPlaylists": "你沒有播放列表", + "MessageOr": "或", + "MessagePauseChapter": "暫停章節播放", + "MessagePlayChapter": "開始章節播放", + "MessagePlaylistCreateFromCollection": "從收藏中創建播放列表", + "MessagePodcastHasNoRSSFeedForMatching": "播客沒有可用於匹配 RSS 源的 url", + "MessageQuickMatchDescription": "使用來自 '{0}' 的第一個匹配結果填充空白詳細信息和封面. 除非啟用 '首選匹配元數據' 伺服器設定, 否則不會覆蓋詳細信息.", + "MessageRemoveChapter": "移除章節", + "MessageRemoveEpisodes": "移除 {0} 劇集", + "MessageRemoveFromPlayerQueue": "從播放佇列中移除", + "MessageRemoveUserWarning": "是否確實要永久刪除使用者 \"{0}\"?", + "MessageReportBugsAndContribute": "報告錯誤、請求功能和貢獻在", + "MessageResetChaptersConfirm": "你確定要重置章節並撤消你所做的更改嗎?", + "MessageRestoreBackupConfirm": "你確定要恢復創建的這個備份", + "MessageRestoreBackupWarning": "恢復備份將覆蓋位於 /config 的整個資料庫並覆蓋 /metadata/items & /metadata/authors 中的圖像.<br /><br />備份不會修改媒體庫資料夾中的任何檔案. 如果您已啟用伺服器設定將封面和元數據存儲在庫資料夾中,則不會備份或覆蓋這些內容.<br /><br />將自動刷新使用伺服器的所有客戶端.", + "MessageSearchResultsFor": "搜尋結果", + "MessageSelected": "{0} 被選取", + "MessageServerCouldNotBeReached": "無法連接伺服器", + "MessageSetChaptersFromTracksDescription": "把每個音頻檔設定為章節並將章節標題設定為音頻檔名", + "MessageStartPlaybackAtTime": "開始播放 \"{0}\" 在 {1}?", + "MessageThinking": "正在查找...", + "MessageUploaderItemFailed": "上傳失敗", + "MessageUploaderItemSuccess": "上傳成功!", + "MessageUploading": "正在上傳...", + "MessageValidCronExpression": "有效的計劃任務表達式", + "MessageWatcherIsDisabledGlobally": "在伺服器設定中禁用全域監視程序", + "MessageXLibraryIsEmpty": "{0} 庫為空!", + "MessageYourAudiobookDurationIsLonger": "您的有聲書持續時間比找到的持續時間長", + "MessageYourAudiobookDurationIsShorter": "您的有聲書持續時間比找到的持續時間短", + "NoteChangeRootPassword": "Root 是唯一可以擁有空密碼的使用者", + "NoteChapterEditorTimes": "注意: 第一章開始時間必須保持在 0:00, 最後一章開始時間不能超過有聲書持續時間.", + "NoteFolderPicker": "注意: 將不顯示已映射的資料夾", + "NoteRSSFeedPodcastAppsHttps": "警告: 大多數播客應用程序都需要 RSS 源 URL 使用 HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "警告: 您的一集或多集沒有發布日期. 一些播客應用程序要求這樣做.", + "NoteUploaderFoldersWithMediaFiles": "包含媒體檔案的資料夾將作為單獨的媒體庫項目處理.", + "NoteUploaderOnlyAudioFiles": "如果只上傳音頻檔, 則每個音頻檔將作為單獨的有聲書處理.", + "NoteUploaderUnsupportedFiles": "不支援的檔案將被忽略. 選擇或刪除資料夾時, 將忽略不在項目資料夾中的其他檔案.", + "PlaceholderNewCollection": "輸入收藏夾名稱", + "PlaceholderNewFolderPath": "輸入資料夾路徑", + "PlaceholderNewPlaylist": "輸入播放列表名稱", + "PlaceholderSearch": "查找..", + "PlaceholderSearchEpisode": "搜尋劇集..", + "ToastAccountUpdateFailed": "帳號更新失敗", + "ToastAccountUpdateSuccess": "帳號已更新", + "ToastAuthorImageRemoveFailed": "作者圖像刪除失敗", + "ToastAuthorImageRemoveSuccess": "作者圖像已刪除", + "ToastAuthorUpdateFailed": "作者更新失敗", + "ToastAuthorUpdateMerged": "作者已合併", + "ToastAuthorUpdateSuccess": "作者已更新", + "ToastAuthorUpdateSuccessNoImageFound": "作者已更新 (未找到圖像)", + "ToastBackupCreateFailed": "備份創建失敗", + "ToastBackupCreateSuccess": "備份已創建", + "ToastBackupDeleteFailed": "備份刪除失敗", + "ToastBackupDeleteSuccess": "備份已刪除", + "ToastBackupRestoreFailed": "備份還原失敗", + "ToastBackupUploadFailed": "上傳備份失敗", + "ToastBackupUploadSuccess": "備份已上傳", + "ToastBatchUpdateFailed": "批量更新失敗", + "ToastBatchUpdateSuccess": "批量更新成功", + "ToastBookmarkCreateFailed": "創建書籤失敗", + "ToastBookmarkCreateSuccess": "書籤已新增", + "ToastBookmarkRemoveFailed": "書籤刪除失敗", + "ToastBookmarkRemoveSuccess": "書籤已刪除", + "ToastBookmarkUpdateFailed": "書籤更新失敗", + "ToastBookmarkUpdateSuccess": "書籤已更新", + "ToastChaptersHaveErrors": "章節有錯誤", + "ToastChaptersMustHaveTitles": "章節必須有標題", + "ToastCollectionItemsRemoveFailed": "從收藏夾移除項目失敗", + "ToastCollectionItemsRemoveSuccess": "項目從收藏夾移除", + "ToastCollectionRemoveFailed": "刪除收藏夾失敗", + "ToastCollectionRemoveSuccess": "收藏夾已刪除", + "ToastCollectionUpdateFailed": "更新收藏夾失敗", + "ToastCollectionUpdateSuccess": "收藏夾已更新", + "ToastItemCoverUpdateFailed": "更新項目封面失敗", + "ToastItemCoverUpdateSuccess": "項目封面已更新", + "ToastItemDetailsUpdateFailed": "更新項目詳細信息失敗", + "ToastItemDetailsUpdateSuccess": "項目詳細信息已更新", + "ToastItemDetailsUpdateUnneeded": "項目詳細信息無需更新", + "ToastItemMarkedAsFinishedFailed": "標記為聽完失敗", + "ToastItemMarkedAsFinishedSuccess": "標記為聽完的項目", + "ToastItemMarkedAsNotFinishedFailed": "標記為未聽完失敗", + "ToastItemMarkedAsNotFinishedSuccess": "標記為未聽完的項目", + "ToastLibraryCreateFailed": "創建媒體庫失敗", + "ToastLibraryCreateSuccess": "媒體庫 \"{0}\" 創建成功", + "ToastLibraryDeleteFailed": "刪除媒體庫失敗", + "ToastLibraryDeleteSuccess": "媒體庫已刪除", + "ToastLibraryScanFailedToStart": "無法啟動掃描", + "ToastLibraryScanStarted": "媒體庫掃描已啟動", + "ToastLibraryUpdateFailed": "更新圖書庫失敗", + "ToastLibraryUpdateSuccess": "媒體庫 \"{0}\" 已更新", + "ToastPlaylistCreateFailed": "創建播放列表失敗", + "ToastPlaylistCreateSuccess": "已成功創建播放列表", + "ToastPlaylistRemoveFailed": "刪除播放列表失敗", + "ToastPlaylistRemoveSuccess": "播放列表已刪除", + "ToastPlaylistUpdateFailed": "更新播放列表失敗", + "ToastPlaylistUpdateSuccess": "播放列表已更新", + "ToastPodcastCreateFailed": "創建播客失敗", + "ToastPodcastCreateSuccess": "已成功創建播客", + "ToastRemoveItemFromCollectionFailed": "從收藏中刪除項目失敗", + "ToastRemoveItemFromCollectionSuccess": "項目已從收藏中刪除", + "ToastRSSFeedCloseFailed": "關閉 RSS 源失敗", + "ToastRSSFeedCloseSuccess": "RSS 源已關閉", + "ToastSendEbookToDeviceFailed": "發送電子書到設備失敗", + "ToastSendEbookToDeviceSuccess": "電子書已經發送到設備 \"{0}\"", + "ToastSeriesUpdateFailed": "更新系列失敗", + "ToastSeriesUpdateSuccess": "系列已更新", + "ToastSessionDeleteFailed": "刪除會話失敗", + "ToastSessionDeleteSuccess": "會話已刪除", + "ToastSocketConnected": "網路已連接", + "ToastSocketDisconnected": "網路已斷開", + "ToastSocketFailedToConnect": "網路連接失敗", + "ToastUserDeleteFailed": "刪除使用者失敗", + "ToastUserDeleteSuccess": "使用者已刪除" +} From 76100846272958ddd3dc437edc1b047e6a5b847e Mon Sep 17 00:00:00 2001 From: -Shiken- <58270741+den13501@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:39:01 +0800 Subject: [PATCH 0431/2145] Update zh-tw.json fix --- client/strings/zh-tw.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/zh-tw.json b/client/strings/zh-tw.json index 64715680..5a9afc98 100644 --- a/client/strings/zh-tw.json +++ b/client/strings/zh-tw.json @@ -23,7 +23,7 @@ "ButtonCreate": "創建", "ButtonCreateBackup": "創建備份", "ButtonDelete": "刪除", - "ButtonDownloadQueue": "下載佇列, + "ButtonDownloadQueue": "下載佇列", "ButtonEdit": "編輯", "ButtonEditChapters": "編輯章節", "ButtonEditPodcast": "編輯播客", From 94d1732b0d8ecc5da65bebe07c6313535c8e6f7d Mon Sep 17 00:00:00 2001 From: Kaldigo <6437691+kaldigo@users.noreply.github.com> Date: Tue, 12 Mar 2024 08:18:52 +0000 Subject: [PATCH 0432/2145] Added isbn to CustomProviderAdapter --- server/finders/BookFinder.js | 7 ++++--- server/providers/CustomProviderAdapter.js | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index f218587c..2c6fc9ee 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -152,11 +152,12 @@ class BookFinder { /** * * @param {string} title - * @param {string} author + * @param {string} author + * @param {string} isbn * @param {string} providerSlug * @returns {Promise<Object[]>} */ - async getCustomProviderResults(title, author, providerSlug) { + async getCustomProviderResults(title, author, isbn, providerSlug) { const books = await this.customProviderAdapter.search(title, author, providerSlug, 'book') if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`) @@ -333,7 +334,7 @@ class BookFinder { // Custom providers are assumed to be correct if (provider.startsWith('custom-')) { - return this.getCustomProviderResults(title, author, provider) + return this.getCustomProviderResults(title, author, isbn, provider) } if (!title) diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js index 3d5209c7..1ec75457 100644 --- a/server/providers/CustomProviderAdapter.js +++ b/server/providers/CustomProviderAdapter.js @@ -9,11 +9,12 @@ class CustomProviderAdapter { * * @param {string} title * @param {string} author + * @param {string} isbn * @param {string} providerSlug * @param {string} mediaType * @returns {Promise<Object[]>} */ - async search(title, author, providerSlug, mediaType) { + async search(title, author, isbn, providerSlug, mediaType) { const providerId = providerSlug.split('custom-')[1] const provider = await Database.customMetadataProviderModel.findByPk(providerId) @@ -29,6 +30,9 @@ class CustomProviderAdapter { if (author) { queryObj.author = author } + if (isbn) { + queryObj.isbn = isbn + } const queryString = (new URLSearchParams(queryObj)).toString() // Setup headers From 29e9216bb1a0b0772601438905ac68977febed0a Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Tue, 12 Mar 2024 13:17:52 +0200 Subject: [PATCH 0433/2145] Make series sequence cleanup slighlty less aggressive --- server/providers/Audible.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/providers/Audible.js b/server/providers/Audible.js index e46ed323..a9e7b0cd 100644 --- a/server/providers/Audible.js +++ b/server/providers/Audible.js @@ -30,9 +30,7 @@ class Audible { cleanSeriesSequence(seriesName, sequence) { if (!sequence) return '' let updatedSequence = sequence.replace(/Book /, '').trim() - if (updatedSequence.includes(' ')) { - updatedSequence = updatedSequence.split(' ').shift().replace(/,$/, '') - } + updatedSequence = updatedSequence.replace(/(\d+)(, .*)/, '$1').trim() if (sequence !== updatedSequence) { Logger.debug(`[Audible] Series "${seriesName}" sequence was cleaned from "${sequence}" to "${updatedSequence}"`) } From a814e4515034a8910d3f755ca76d067a201aeb4e Mon Sep 17 00:00:00 2001 From: Lauri Vuorela <lauri.vuorela@gmail.com> Date: Tue, 12 Mar 2024 17:00:21 +0100 Subject: [PATCH 0434/2145] add a toggle for the new continue series setting --- client/components/modals/libraries/EditModal.vue | 1 + .../modals/libraries/LibrarySettings.vue | 14 ++++++++++++++ client/strings/en-us.json | 2 ++ 3 files changed, 17 insertions(+) diff --git a/client/components/modals/libraries/EditModal.vue b/client/components/modals/libraries/EditModal.vue index 2a68dd63..27e3ec6d 100644 --- a/client/components/modals/libraries/EditModal.vue +++ b/client/components/modals/libraries/EditModal.vue @@ -127,6 +127,7 @@ export default { skipMatchingMediaWithIsbn: false, autoScanCronExpression: null, hideSingleBookSeries: false, + onlyShowLaterBooksInContinueSeries: false, metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] } } diff --git a/client/components/modals/libraries/LibrarySettings.vue b/client/components/modals/libraries/LibrarySettings.vue index 4712d6a2..5a8a7a40 100644 --- a/client/components/modals/libraries/LibrarySettings.vue +++ b/client/components/modals/libraries/LibrarySettings.vue @@ -49,6 +49,17 @@ </ui-tooltip> </div> </div> + <div v-if="isBookLibrary" class="py-3"> + <div class="flex items-center"> + <ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" @input="formUpdated" /> + <ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp"> + <p class="pl-4 text-base"> + {{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }} + <span class="material-icons icon-text text-sm">info_outlined</span> + </p> + </ui-tooltip> + </div> + </div> <div v-if="isPodcastLibrary" class="py-3"> <ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-52" @input="formUpdated" /> </div> @@ -73,6 +84,7 @@ export default { skipMatchingMediaWithIsbn: false, audiobooksOnly: false, hideSingleBookSeries: false, + onlyShowLaterBooksInContinueSeries: false, podcastSearchRegion: 'us' } }, @@ -107,6 +119,7 @@ export default { skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn, audiobooksOnly: !!this.audiobooksOnly, hideSingleBookSeries: !!this.hideSingleBookSeries, + onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries, podcastSearchRegion: this.podcastSearchRegion } } @@ -121,6 +134,7 @@ export default { this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn this.audiobooksOnly = !!this.librarySettings.audiobooksOnly this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries + this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us' } }, diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 2465f873..b5cf5ecd 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHomePageBookshelfView": "Home page use bookshelf view", "LabelSettingsLibraryBookshelfView": "Library use bookshelf view", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Avoids showing books that are earlier in a series than the ones you have already read.", "LabelSettingsParseSubtitles": "Parse subtitles", "LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"", "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata", From c83399c7b5de67103d508ac2d758e8d4e3d88672 Mon Sep 17 00:00:00 2001 From: Lauri Vuorela <lauri.vuorela@gmail.com> Date: Tue, 12 Mar 2024 17:04:26 +0100 Subject: [PATCH 0435/2145] use the toggle to not show earlier works than the ones already read --- server/models/Library.js | 1 + server/objects/settings/LibrarySettings.js | 5 +++- .../utils/queries/libraryItemsBookFilters.js | 24 +++++++++++++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/server/models/Library.js b/server/models/Library.js index c6875ad7..49b54d68 100644 --- a/server/models/Library.js +++ b/server/models/Library.js @@ -11,6 +11,7 @@ const oldLibrary = require('../objects/Library') * @property {string} autoScanCronExpression * @property {boolean} audiobooksOnly * @property {boolean} hideSingleBookSeries Do not show series that only have 1 book + * @property {boolean} onlyShowLaterBooksInContinueSeries Skip showing books that are earlier than the max sequence read * @property {string[]} metadataPrecedence */ diff --git a/server/objects/settings/LibrarySettings.js b/server/objects/settings/LibrarySettings.js index b070ff79..a9885c79 100644 --- a/server/objects/settings/LibrarySettings.js +++ b/server/objects/settings/LibrarySettings.js @@ -8,7 +8,8 @@ class LibrarySettings { this.skipMatchingMediaWithIsbn = false this.autoScanCronExpression = null this.audiobooksOnly = false - this.hideSingleBookSeries = false // Do not show series that only have 1 book + this.hideSingleBookSeries = false // Do not show series that only have 1 book + this.onlyShowLaterBooksInContinueSeries = false // Skip showing books that are earlier than the max sequence read this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] this.podcastSearchRegion = 'us' @@ -25,6 +26,7 @@ class LibrarySettings { this.autoScanCronExpression = settings.autoScanCronExpression || null this.audiobooksOnly = !!settings.audiobooksOnly this.hideSingleBookSeries = !!settings.hideSingleBookSeries + this.onlyShowLaterBooksInContinueSeries = !!settings.onlyShowLaterBooksInContinueSeries if (settings.metadataPrecedence) { this.metadataPrecedence = [...settings.metadataPrecedence] } else { @@ -43,6 +45,7 @@ class LibrarySettings { autoScanCronExpression: this.autoScanCronExpression, audiobooksOnly: this.audiobooksOnly, hideSingleBookSeries: this.hideSingleBookSeries, + onlyShowLaterBooksInContinueSeries: this.onlyShowLaterBooksInContinueSeries, metadataPrecedence: [...this.metadataPrecedence], podcastSearchRegion: this.podcastSearchRegion } diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 16a25847..dc48c89c 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -676,11 +676,14 @@ module.exports = { ], attributes: { include: [ - [Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress'] + [Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress'], + [Sequelize.literal('(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'maxSequence'], + [Sequelize.literal('(SELECT json_extract(libraries.settings, "$.onlyShowLaterBooksInContinueSeries") FROM libraries WHERE id = :libraryId)'), 'onlyShowLaterBooksInContinueSeries'] ] }, replacements: { userId: user.id, + libraryId: libraryId, ...userPermissionBookWhere.replacements }, include: { @@ -731,13 +734,26 @@ module.exports = { const libraryItems = series.map(s => { if (!s.bookSeries.length) return null // this is only possible if user has restricted books in series - const libraryItem = s.bookSeries[0].book.libraryItem.toJSON() - const book = s.bookSeries[0].book.toJSON() + + var bookIndex = 0 + // if the library setting is toggled, only show later entries in series, otherwise skip + if (s.dataValues.onlyShowLaterBooksInContinueSeries === 1) { + bookIndex = s.bookSeries.findIndex(function (b) { + return parseFloat(b.dataValues.sequence) > s.dataValues.maxSequence + }) + if (bookIndex === -1) { + // no later books than maxSequence + return null + } + } + + const libraryItem = s.bookSeries[bookIndex].book.libraryItem.toJSON() + const book = s.bookSeries[bookIndex].book.toJSON() delete book.libraryItem libraryItem.series = { id: s.id, name: s.name, - sequence: s.bookSeries[0].sequence + sequence: s.bookSeries[bookIndex].sequence } if (libraryItem.feeds?.length) { libraryItem.rssFeed = libraryItem.feeds[0] From 2a722ab1635abb3b913a8c61fcdeda95370fd00f Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Tue, 12 Mar 2024 18:07:13 +0100 Subject: [PATCH 0436/2145] Auth: Fix crash on missing logout URL When using OpenID Also added debug information on openid errors --- server/Auth.js | 55 ++++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index ad219dc2..352faf66 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -429,7 +429,7 @@ class Auth { // 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()) + Logger.debug(header + '\n' + response.body?.toString() + '\n' + JSON.stringify(response.body, null, 2)) } if (isMobile) { @@ -533,42 +533,45 @@ class Auth { res.clearCookie('auth_method') + let logoutUrl = null + if (authMethod === 'openid' || authMethod === 'openid-mobile') { // If we are using openid, we need to redirect to the logout endpoint // node-openid-client does not support doing it over passport const oidcStrategy = passport._strategy('openid-client') const client = oidcStrategy._client - let postLogoutRedirectUri = null + if (client.issuer.end_session_endpoint && client.issuer.end_session_endpoint.length > 0) { + let postLogoutRedirectUri = null - if (authMethod === 'openid') { - const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http' - const host = req.get('host') - // TODO: ABS does currently not support subfolders for installation - // If we want to support it we need to include a config for the serverurl - postLogoutRedirectUri = `${protocol}://${host}/login` + if (authMethod === 'openid') { + const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http' + const host = req.get('host') + // TODO: ABS does currently not support subfolders for installation + // If we want to support it we need to include a config for the serverurl + postLogoutRedirectUri = `${protocol}://${host}/login` + } + // else for openid-mobile we keep postLogoutRedirectUri on null + // nice would be to redirect to the app here, but for example Authentik does not implement + // the post_logout_redirect_uri parameter at all and for other providers + // we would also need again to implement (and even before get to know somehow for 3rd party apps) + // the correct app link like audiobookshelf://login (and maybe also provide a redirect like mobile-redirect). + // Instead because its null (and this way the parameter will be omitted completly), the client/app can simply append something like + // &post_logout_redirect_uri=audiobookshelf://login to the received logout url by itself which is the simplest solution + // (The URL needs to be whitelisted in the config of the SSO/ID provider) + + logoutUrl = client.endSessionUrl({ + id_token_hint: req.cookies.openid_id_token, + post_logout_redirect_uri: postLogoutRedirectUri + }) } - // else for openid-mobile we keep postLogoutRedirectUri on null - // nice would be to redirect to the app here, but for example Authentik does not implement - // the post_logout_redirect_uri parameter at all and for other providers - // we would also need again to implement (and even before get to know somehow for 3rd party apps) - // the correct app link like audiobookshelf://login (and maybe also provide a redirect like mobile-redirect). - // Instead because its null (and this way the parameter will be omitted completly), the client/app can simply append something like - // &post_logout_redirect_uri=audiobookshelf://login to the received logout url by itself which is the simplest solution - // (The URL needs to be whitelisted in the config of the SSO/ID provider) - - const logoutUrl = client.endSessionUrl({ - id_token_hint: req.cookies.openid_id_token, - post_logout_redirect_uri: postLogoutRedirectUri - }) res.clearCookie('openid_id_token') - - // Tell the user agent (browser) to redirect to the authentification provider's logout URL - res.send({ redirect_url: logoutUrl }) - } else { - res.sendStatus(200) } + + // Tell the user agent (browser) to redirect to the authentification provider's logout URL + // (or redirect_url: null if we don't have one) + res.send({ redirect_url: logoutUrl }) } }) }) From d6f13513aee29ec5d0a1701c68ef81095ee0c9e8 Mon Sep 17 00:00:00 2001 From: Schiriki <schiriki@raps2.de> Date: Wed, 13 Mar 2024 23:46:56 +0100 Subject: [PATCH 0437/2145] Add name labels to login form --- client/components/ui/TextInput.vue | 5 +++-- client/pages/login.vue | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/client/components/ui/TextInput.vue b/client/components/ui/TextInput.vue index 5f871635..e06740ea 100644 --- a/client/components/ui/TextInput.vue +++ b/client/components/ui/TextInput.vue @@ -1,6 +1,6 @@ <template> <div ref="wrapper" class="relative"> - <input :id="inputId" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" /> + <input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" /> <div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center"> <span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span> </div> @@ -33,6 +33,7 @@ export default { textCenter: Boolean, clearable: Boolean, inputId: String, + inputName: String, step: [String, Number], min: [String, Number] }, @@ -117,4 +118,4 @@ input:read-only { input::-webkit-calendar-picker-indicator { filter: invert(1); } -</style> \ No newline at end of file +</style> diff --git a/client/pages/login.vue b/client/pages/login.vue index 9b7758d8..7f1ff621 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -34,10 +34,10 @@ <form v-show="login_local" @submit.prevent="submitForm"> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label> - <ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" /> + <ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" /> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label> - <ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" /> + <ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" inputName="password" /> <div class="w-full flex justify-end py-3"> <ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn> </div> @@ -281,4 +281,4 @@ export default { this.checkStatus() } } -</script> \ No newline at end of file +</script> From d4c1bc5dfcfe323706604039088359daad879b1a Mon Sep 17 00:00:00 2001 From: Lauri Vuorela <lauri.vuorela@gmail.com> Date: Thu, 14 Mar 2024 09:41:48 +0100 Subject: [PATCH 0438/2145] use already fetched library settings, only fetch maxSequence if setting is turned on --- server/utils/queries/libraryFilters.js | 2 +- .../utils/queries/libraryItemsBookFilters.js | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index 785124a9..9a5dc658 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -127,7 +127,7 @@ module.exports = { * @returns {object} { libraryItems:LibraryItem[], count:number } */ async getLibraryItemsContinueSeries(library, user, include, limit) { - const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library.id, user, include, limit, 0) + const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library, user, include, limit, 0) return { libraryItems: libraryItems.map(li => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index dc48c89c..f1b75699 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -640,7 +640,8 @@ module.exports = { * @param {number} offset * @returns {object} { libraryItems:LibraryItem[], count:number } */ - async getContinueSeriesLibraryItems(libraryId, user, include, limit, offset) { + async getContinueSeriesLibraryItems(library, user, include, limit, offset) { + const libraryId = library.id const libraryItemIncludes = [] if (include.includes('rssfeed')) { libraryItemIncludes.push({ @@ -654,6 +655,13 @@ module.exports = { const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user) bookWhere.push(...userPermissionBookWhere.bookWhere) + let includeAttributes = [ + [Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress'], + ] + if (library.settings.onlyShowLaterBooksInContinueSeries) { + includeAttributes.push([Sequelize.literal('(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'maxSequence']) + } + const { rows: series, count } = await Database.seriesModel.findAndCountAll({ where: [ { @@ -675,11 +683,7 @@ module.exports = { Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM mediaProgresses mp, bookSeries bs WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id AND mp.isFinished = 0 AND mp.currentTime > 0)`), 0) ], attributes: { - include: [ - [Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress'], - [Sequelize.literal('(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'maxSequence'], - [Sequelize.literal('(SELECT json_extract(libraries.settings, "$.onlyShowLaterBooksInContinueSeries") FROM libraries WHERE id = :libraryId)'), 'onlyShowLaterBooksInContinueSeries'] - ] + include: includeAttributes }, replacements: { userId: user.id, @@ -737,7 +741,7 @@ module.exports = { var bookIndex = 0 // if the library setting is toggled, only show later entries in series, otherwise skip - if (s.dataValues.onlyShowLaterBooksInContinueSeries === 1) { + if (library.settings.onlyShowLaterBooksInContinueSeries) { bookIndex = s.bookSeries.findIndex(function (b) { return parseFloat(b.dataValues.sequence) > s.dataValues.maxSequence }) From 65153fae9da30a5528380d279cfde1aab4652603 Mon Sep 17 00:00:00 2001 From: Lauri Vuorela <lauri.vuorela@gmail.com> Date: Thu, 14 Mar 2024 09:42:50 +0100 Subject: [PATCH 0439/2145] var => let --- server/utils/queries/libraryItemsBookFilters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index f1b75699..0cb3ffc3 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -739,7 +739,7 @@ module.exports = { const libraryItems = series.map(s => { if (!s.bookSeries.length) return null // this is only possible if user has restricted books in series - var bookIndex = 0 + let bookIndex = 0 // if the library setting is toggled, only show later entries in series, otherwise skip if (library.settings.onlyShowLaterBooksInContinueSeries) { bookIndex = s.bookSeries.findIndex(function (b) { From 8826d3af62ec534ca23706a41370a7720cb34963 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 14 Mar 2024 19:36:51 +0200 Subject: [PATCH 0440/2145] fix cleanSeriesSequence method to extract first numeric value --- server/providers/Audible.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/providers/Audible.js b/server/providers/Audible.js index a9e7b0cd..de98f67b 100644 --- a/server/providers/Audible.js +++ b/server/providers/Audible.js @@ -29,8 +29,9 @@ class Audible { */ cleanSeriesSequence(seriesName, sequence) { if (!sequence) return '' - let updatedSequence = sequence.replace(/Book /, '').trim() - updatedSequence = updatedSequence.replace(/(\d+)(, .*)/, '$1').trim() + // match any number with optional decimal (e.g, 1 or 1.5 or .5) + let numberFound = sequence.match(/\.\d+|\d+(?:\.\d+)?/) + let updatedSequence = numberFound ? numberFound[0] : sequence if (sequence !== updatedSequence) { Logger.debug(`[Audible] Series "${seriesName}" sequence was cleaned from "${sequence}" to "${updatedSequence}"`) } From ae395497a5da9cbec9940a675d78d1b43bee91fa Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 14 Mar 2024 19:37:51 +0200 Subject: [PATCH 0441/2145] Add tests for cleanSeriesSequence --- test/server/providers/Audible.test.js | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 test/server/providers/Audible.test.js diff --git a/test/server/providers/Audible.test.js b/test/server/providers/Audible.test.js new file mode 100644 index 00000000..67e85111 --- /dev/null +++ b/test/server/providers/Audible.test.js @@ -0,0 +1,48 @@ +const Audible = require('../../../server/providers/Audible') +const { expect } = require('chai') +const sinon = require('sinon') + +describe('Audible', () => { + let audible; + + beforeEach(() => { + audible = new Audible(); + }); + + describe('cleanSeriesSequence', () => { + it('should return an empty string if sequence is falsy', () => { + const result = audible.cleanSeriesSequence('Series Name', null) + expect(result).to.equal('') + }) + + it('should return the sequence as is if it does not contain a number', () => { + const result = audible.cleanSeriesSequence('Series Name', 'part a') + expect(result).to.equal('part a') + }) + + it('should return the sequence as is if contains just a number', () => { + const result = audible.cleanSeriesSequence('Series Name', '2') + expect(result).to.equal('2') + }) + + it('should return the sequence as is if contains just a number with decimals', () => { + const result = audible.cleanSeriesSequence('Series Name', '2.3') + expect(result).to.equal('2.3') + }) + + it('should extract and return the first number from the sequence', () => { + const result = audible.cleanSeriesSequence('Series Name', 'Book 1') + expect(result).to.equal('1') + }) + + it('should extract and return the number with decimals from the sequence', () => { + const result = audible.cleanSeriesSequence('Series Name', 'Book 1.5') + expect(result).to.equal('1.5') + }) + + it('should extract and return the number even if it has no leading zero', () => { + const result = audible.cleanSeriesSequence('Series Name', 'Book .5') + expect(result).to.equal('.5') + }) + }) +}) \ No newline at end of file From f7ae7783bd6dfa3d2d2d998a7b05c59c352f3766 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 14 Mar 2024 19:58:42 +0200 Subject: [PATCH 0442/2145] Fix broken BinaryManager.isBinaryGood test --- test/server/managers/BinaryManager.test.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/server/managers/BinaryManager.test.js b/test/server/managers/BinaryManager.test.js index 48aa5e3f..e13b0ced 100644 --- a/test/server/managers/BinaryManager.test.js +++ b/test/server/managers/BinaryManager.test.js @@ -291,6 +291,7 @@ describe('isBinaryGood', () => { const binaryPath = '/path/to/binary' const execCommand = '"' + binaryPath + '"' + ' -version' + const goodVersions = ['5.1', '6'] beforeEach(() => { binaryManager = new BinaryManager() @@ -306,7 +307,7 @@ describe('isBinaryGood', () => { it('should return false if binaryPath is falsy', async () => { fsPathExistsStub.resolves(true) - const result = await binaryManager.isBinaryGood(null) + const result = await binaryManager.isBinaryGood(null, goodVersions) expect(result).to.be.false expect(fsPathExistsStub.called).to.be.false @@ -316,7 +317,7 @@ describe('isBinaryGood', () => { it('should return false if binaryPath does not exist', async () => { fsPathExistsStub.resolves(false) - const result = await binaryManager.isBinaryGood(binaryPath) + const result = await binaryManager.isBinaryGood(binaryPath, goodVersions) expect(result).to.be.false expect(fsPathExistsStub.calledOnce).to.be.true @@ -328,7 +329,7 @@ describe('isBinaryGood', () => { fsPathExistsStub.resolves(true) execStub.rejects(new Error('Failed to execute command')) - const result = await binaryManager.isBinaryGood(binaryPath) + const result = await binaryManager.isBinaryGood(binaryPath, goodVersions) expect(result).to.be.false expect(fsPathExistsStub.calledOnce).to.be.true @@ -342,7 +343,7 @@ describe('isBinaryGood', () => { fsPathExistsStub.resolves(true) execStub.resolves({ stdout }) - const result = await binaryManager.isBinaryGood(binaryPath) + const result = await binaryManager.isBinaryGood(binaryPath, goodVersions) expect(result).to.be.false expect(fsPathExistsStub.calledOnce).to.be.true @@ -356,7 +357,7 @@ describe('isBinaryGood', () => { fsPathExistsStub.resolves(true) execStub.resolves({ stdout }) - const result = await binaryManager.isBinaryGood(binaryPath) + const result = await binaryManager.isBinaryGood(binaryPath, goodVersions) expect(result).to.be.false expect(fsPathExistsStub.calledOnce).to.be.true @@ -370,7 +371,7 @@ describe('isBinaryGood', () => { fsPathExistsStub.resolves(true) execStub.resolves({ stdout }) - const result = await binaryManager.isBinaryGood(binaryPath) + const result = await binaryManager.isBinaryGood(binaryPath, goodVersions) expect(result).to.be.true expect(fsPathExistsStub.calledOnce).to.be.true From d02fc2debe3ca91c467113d7d03fc7e13bc684fb Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 14 Mar 2024 14:27:33 -0500 Subject: [PATCH 0443/2145] Update continue series skip earlier books query attribute to look for finished books, update wording on help text, map translations --- client/strings/cs.json | 2 ++ client/strings/da.json | 2 ++ client/strings/de.json | 2 ++ client/strings/en-us.json | 2 +- client/strings/es.json | 2 ++ client/strings/et.json | 2 ++ client/strings/fr.json | 4 +++- client/strings/gu.json | 2 ++ client/strings/hi.json | 2 ++ client/strings/hr.json | 2 ++ client/strings/hu.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/pt-br.json | 4 +++- client/strings/ru.json | 2 ++ client/strings/sv.json | 2 ++ client/strings/vi-vn.json | 4 +++- client/strings/zh-cn.json | 2 ++ server/utils/queries/libraryItemsBookFilters.js | 2 +- 22 files changed, 45 insertions(+), 5 deletions(-) diff --git a/client/strings/cs.json b/client/strings/cs.json index 7dc18aa6..4ce358a2 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -466,6 +466,8 @@ "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", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "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", diff --git a/client/strings/da.json b/client/strings/da.json index 8c305ca2..de00a1fc 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Serier med en enkelt bog vil blive skjult fra serie-siden og hjemmesidehylder.", "LabelSettingsHomePageBookshelfView": "Brug bogreolvisning på startside", "LabelSettingsLibraryBookshelfView": "Brug bogreolvisning i biblioteket", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "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", diff --git a/client/strings/de.json b/client/strings/de.json index 33b0fb1b..ed99f095 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Serien, die nur ein einzelnes Buch enthalten, werden auf der Startseite und in der Serienansicht ausgeblendet.", "LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht", "LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Analysiere Untertitel", "LabelSettingsParseSubtitlesHelp": "Extrahiere den Untertitel von Medium-Ordnernamen.<br>Untertitel müssen vom eigentlichem Titel durch ein \" - \" getrennt sein. <br>Beispiel: \"Titel - Untertitel\"", "LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index b5cf5ecd..43a1ef44 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -467,7 +467,7 @@ "LabelSettingsHomePageBookshelfView": "Home page use bookshelf view", "LabelSettingsLibraryBookshelfView": "Library use bookshelf view", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", - "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Avoids showing books that are earlier in a series than the ones you have already read.", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Parse subtitles", "LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"", "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata", diff --git a/client/strings/es.json b/client/strings/es.json index 452b625d..068c5bc4 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -466,6 +466,8 @@ "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", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Extraer Subtítulos", "LabelSettingsParseSubtitlesHelp": "Extraer subtítulos de los nombres de las carpetas de los audiolibros.<br>Los subtítulos deben estar separados por \" - \"<br>Por ejemplo: \"Ejemplo de Título - Subtítulo Aquí\" tiene el subtítulo \"Subtítulo Aquí\"", "LabelSettingsPreferMatchedMetadata": "Preferir metadatos encontrados", diff --git a/client/strings/et.json b/client/strings/et.json index 8c13c435..da145027 100644 --- a/client/strings/et.json +++ b/client/strings/et.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Ühe raamatuga seeriaid peidetakse seeria lehelt ja avalehe riiulitelt.", "LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet", "LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Lugege subtiitreid", "LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest.<br>Subtiitrid peavad olema eraldatud \" - \".<br>Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"", "LabelSettingsPreferMatchedMetadata": "Eelista sobitatud metaandmeid", diff --git a/client/strings/fr.json b/client/strings/fr.json index 2948168e..b7b28472 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent qu’un seul livre seront masquées sur la page de la série et sur les étagères de la page d’accueil.", "LabelSettingsHomePageBookshelfView": "La page d’accueil utilise la vue étagère", "LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Analyser les sous-titres", "LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du livre audio.<br>Les sous-titres doivent être séparés par « - »<br>c’est-à-dire : « Titre du livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »", "LabelSettingsPreferMatchedMetadata": "Préférer les métadonnées par correspondance", @@ -776,4 +778,4 @@ "ToastSocketFailedToConnect": "Échec de la connexion WebSocket", "ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur", "ToastUserDeleteSuccess": "Utilisateur supprimé" -} +} \ No newline at end of file diff --git a/client/strings/gu.json b/client/strings/gu.json index eff18343..0a2dc3b6 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHomePageBookshelfView": "Home page use bookshelf view", "LabelSettingsLibraryBookshelfView": "Library use bookshelf view", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Parse subtitles", "LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"", "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata", diff --git a/client/strings/hi.json b/client/strings/hi.json index d126e9f5..4e8aae7b 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHomePageBookshelfView": "Home page use bookshelf view", "LabelSettingsLibraryBookshelfView": "Library use bookshelf view", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Parse subtitles", "LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"", "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata", diff --git a/client/strings/hr.json b/client/strings/hr.json index 313505ac..fa5bcd35 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHomePageBookshelfView": "Koristi bookshelf pogled za početnu stranicu", "LabelSettingsLibraryBookshelfView": "Koristi bookshelf pogled za biblioteku", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Parsaj podnapise", "LabelSettingsParseSubtitlesHelp": "Izvadi podnapise iz imena od audiobook foldera.<br>Podnapis mora biti odvojen sa \" - \"<br>npr. \"Ime knjige - Podnapis ovdje\" ima podnapis \"Podnapis ovdje\"", "LabelSettingsPreferMatchedMetadata": "Preferiraj matchane metapodatke", diff --git a/client/strings/hu.json b/client/strings/hu.json index 1829916a..f7e2abd8 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "A csak egy könyvet tartalmazó sorozatok el lesznek rejtve a sorozatok oldalról és a kezdőlap polcairól.", "LabelSettingsHomePageBookshelfView": "Kezdőlap használja a könyvespolc nézetet", "LabelSettingsLibraryBookshelfView": "Könyvtár használja a könyvespolc nézetet", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Feliratok elemzése", "LabelSettingsParseSubtitlesHelp": "Feliratok kinyerése a hangoskönyv mappaneveiből.<br>A feliratnak el kell különülnie egy \" - \" jellel<br>például: \"Könyv címe - Egy felirat itt\" esetén a felirat \"Egy felirat itt\"", "LabelSettingsPreferMatchedMetadata": "Preferált egyeztetett metaadatok", diff --git a/client/strings/it.json b/client/strings/it.json index f4e97048..8924a28b 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Le serie che hanno un solo libro saranno nascoste dalla pagina della serie e dagli scaffali della home page.", "LabelSettingsHomePageBookshelfView": "Home page con sfondo legno", "LabelSettingsLibraryBookshelfView": "Libreria con sfondo legno", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Analizza sottotitoli", "LabelSettingsParseSubtitlesHelp": "Estrai i sottotitoli dai nomi delle cartelle degli audiolibri. <br> I sottotitoli devono essere separati da \" - \"<br> Per esempio \"Il signore degli anelli - Le due Torri \" avrà il sottotitolo \"Le due Torri\"", "LabelSettingsPreferMatchedMetadata": "Preferisci i metadata trovati", diff --git a/client/strings/lt.json b/client/strings/lt.json index 4d97ab47..d36861a4 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Serijos, turinčios tik vieną knygą, bus paslėptos nuo serijų puslapio ir pagrindinio puslapio lentynų.", "LabelSettingsHomePageBookshelfView": "Naudoti pagrindinio puslapio knygų lentynų vaizdą", "LabelSettingsLibraryBookshelfView": "Naudoti bibliotekos knygų lentynų vaizdą", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Analizuoti subtitrus", "LabelSettingsParseSubtitlesHelp": "Išskleisti subtitrus iš audioknygos aplanko pavadinimų.<br>Subtitrai turi būti atskirti brūkšniu \"-\"<br>pavyzdžiui, \"Knygos pavadinimas - Čia yra subtitrai\" turi subtitrą \"Čia yra subtitrai\"", "LabelSettingsPreferMatchedMetadata": "Pirmenybė atitaikytiems metaduomenis", diff --git a/client/strings/nl.json b/client/strings/nl.json index 858b3de6..b8d3f669 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Series die slechts een enkel boek bevatten worden verborgen op de seriespagina en de homepagina-planken.", "LabelSettingsHomePageBookshelfView": "Boekenplank-view voor homepagina", "LabelSettingsLibraryBookshelfView": "Boekenplank-view voor bibliotheek", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Parseer subtitel", "LabelSettingsParseSubtitlesHelp": "Haal subtitels uit mapnaam van audioboek.<br>Subtitel moet gescheiden zijn met \" - \"<br>b.v. \"Boektitel - Een Subtitel Hier\" heeft als subtitel \"Een Subtitel Hier\"", "LabelSettingsPreferMatchedMetadata": "Prefereer gematchte metadata", diff --git a/client/strings/no.json b/client/strings/no.json index 065d28df..818ee0fa 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Serier som har kun en bok vil bli gjemt på serie- og hjemmeside hyllen.", "LabelSettingsHomePageBookshelfView": "Hjemmeside bruk bokhyllevisning", "LabelSettingsLibraryBookshelfView": "Bibliotek bruk bokhyllevisning", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Analyser undertekster", "LabelSettingsParseSubtitlesHelp": "Trekk ut undertekster fra lydbok mappenavn.<br>undertekster må være separert med \" - \"<br>f.eks. \"Boktittel - Undertekst her\" har Undertekst \"Undertekst her\"", "LabelSettingsPreferMatchedMetadata": "Foretrekk funnet metadata", diff --git a/client/strings/pl.json b/client/strings/pl.json index 2d4141ba..cf4274cc 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHomePageBookshelfView": "Widok półki z książkami na stronie głównej", "LabelSettingsLibraryBookshelfView": "Widok półki z książkami na stronie biblioteki", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Przetwarzaj podtytuły", "LabelSettingsParseSubtitlesHelp": "Opcja pozwala na pobranie podtytułu z nazwy folderu z audiobookiem. <br>Podtytuł musi być rozdzielony za pomocą separatora \" - \"<br>Przykład: \"Book Title - A Subtitle Here\" podtytuł \"A Subtitle Here\"", "LabelSettingsPreferMatchedMetadata": "Preferowanie dopasowanych metadanych", diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 0f0b0f06..7a5a1eec 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Séries com um só livro serão ocultadas na página de séries e na prateleira de séries na página principal.", "LabelSettingsHomePageBookshelfView": "Usar visão estante na página principal", "LabelSettingsLibraryBookshelfView": "Usar visão estante na página da biblioteca", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Analisar subtítulos", "LabelSettingsParseSubtitlesHelp": "Extrair subtítulos do nome da pasta do audiobook.<br>Subtítulo deve estar separado por \" - \"<br>ex: \"Título do Livro - Um Subtítulo Aqui\" tem o subtítulo \"Um Subtítulo Aqui\"", "LabelSettingsPreferMatchedMetadata": "Preferir metadados consultados", @@ -776,4 +778,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} +} \ No newline at end of file diff --git a/client/strings/ru.json b/client/strings/ru.json index 6d690ee6..a55e4668 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Серии, в которых всего одна книга, будут скрыты со страницы серий и полок домашней страницы.", "LabelSettingsHomePageBookshelfView": "Вид книжной полки на Домашней странице", "LabelSettingsLibraryBookshelfView": "Вид книжной полки в Библиотеке", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Разбор подзаголовков", "LabelSettingsParseSubtitlesHelp": "Извлечение подзаголовков из имен папок аудиокниг.<br>Подзаголовок должны быть отделен \" - \"<br>например \"Название Книги - Тут Подзаголовок\" подзаголовок будет \"Тут Подзаголовок\"", "LabelSettingsPreferMatchedMetadata": "Предпочитать метаданные поиска", diff --git a/client/strings/sv.json b/client/strings/sv.json index 55e92be4..820fe18a 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -466,6 +466,8 @@ "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", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "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", diff --git a/client/strings/vi-vn.json b/client/strings/vi-vn.json index 8c84a8c0..bddfd647 100644 --- a/client/strings/vi-vn.json +++ b/client/strings/vi-vn.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Các loạt sách chỉ có một cuốn sách sẽ được ẩn khỏi trang loạt sách và kệ trang chủ.", "LabelSettingsHomePageBookshelfView": "Trang chủ sử dụng chế độ xem kệ sách", "LabelSettingsLibraryBookshelfView": "Thư viện sử dụng chế độ xem kệ sách", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Phân tích phụ đề", "LabelSettingsParseSubtitlesHelp": "Trích xuất phụ đề từ tên thư mục sách nói.<br>Phụ đề phải được tách bằng \" - \"<br>i.e. \"Book Title - A Subtitle Here\" có phụ đề \"A Subtitle Here\"", "LabelSettingsPreferMatchedMetadata": "Ưu tiên siêu dữ liệu phù hợp", @@ -776,4 +778,4 @@ "ToastSocketFailedToConnect": "Không thể kết nối socket", "ToastUserDeleteFailed": "Xóa người dùng thất bại", "ToastUserDeleteSuccess": "Người dùng đã được xóa" -} +} \ No newline at end of file diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 0634c74f..bf816f89 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -466,6 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "只有一本书的系列将从系列页面和主页书架中隐藏.", "LabelSettingsHomePageBookshelfView": "首页使用书架视图", "LabelSettingsLibraryBookshelfView": "媒体库使用书架视图", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "解析副标题", "LabelSettingsParseSubtitlesHelp": "从有声读物文件夹中提取副标题.<br>副标题必须用 \" - \" 分隔.<br>例: \"书名 - 这里是副标题\" 则显示副标题 \"这里是副标题\"", "LabelSettingsPreferMatchedMetadata": "首选匹配的元数据", diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 0cb3ffc3..a9ae00ef 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -659,7 +659,7 @@ module.exports = { [Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress'], ] if (library.settings.onlyShowLaterBooksInContinueSeries) { - includeAttributes.push([Sequelize.literal('(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'maxSequence']) + includeAttributes.push([Sequelize.literal('(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.isFinished = 1 AND mp.userId = :userId AND bs.seriesId = series.id)'), 'maxSequence']) } const { rows: series, count } = await Database.seriesModel.findAndCountAll({ From 521db90ae0873efacaf91e88c3f3db98580b80dd Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 14 Mar 2024 14:37:24 -0500 Subject: [PATCH 0444/2145] Update JSDocs, remove unused libraryId replacement --- server/utils/queries/libraryFilters.js | 6 +++--- server/utils/queries/libraryItemsBookFilters.js | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index 9a5dc658..288ee6db 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -75,7 +75,7 @@ module.exports = { /** * Get library items for most recently added shelf - * @param {oldLibrary} library + * @param {import('../../objects/Library')} library * @param {oldUser} user * @param {string[]} include * @param {number} limit @@ -120,7 +120,7 @@ module.exports = { /** * Get library items for continue series shelf - * @param {string} library + * @param {import('../../objects/Library')} library * @param {oldUser} user * @param {string[]} include * @param {number} limit @@ -145,7 +145,7 @@ module.exports = { /** * Get library items or podcast episodes for the "Listen Again" and "Read Again" shelf - * @param {oldLibrary} library + * @param {import('../../objects/Library')} library * @param {oldUser} user * @param {string[]} include * @param {number} limit diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index a9ae00ef..07a8458d 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -633,12 +633,12 @@ module.exports = { * 2. Has no books in progress * 3. Has at least 1 unfinished book * TODO: Reduce queries - * @param {string} libraryId - * @param {oldUser} user + * @param {import('../../objects/Library')} library + * @param {import('../../objects/user/User')} user * @param {string[]} include * @param {number} limit * @param {number} offset - * @returns {object} { libraryItems:LibraryItem[], count:number } + * @returns {{ libraryItems:import('../../models/LibraryItem')[], count:number }} */ async getContinueSeriesLibraryItems(library, user, include, limit, offset) { const libraryId = library.id @@ -687,7 +687,6 @@ module.exports = { }, replacements: { userId: user.id, - libraryId: libraryId, ...userPermissionBookWhere.replacements }, include: { From 99f0799a116ba09c1d0d4770a71ec8ea2a71a81c Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 14 Mar 2024 16:29:01 -0500 Subject: [PATCH 0445/2145] Update:Adding support for skipping check for ffmpeg/ffprobe binaries with environment variable SKIP_BINARIES_CHECK - Set SKIP_BINARIES_CHECK=1 env variable to skip - Or set SkipBinariesCheck: true in dev.js #2741 --- index.js | 1 + server/managers/BinaryManager.js | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/index.js b/index.js index 79d06c3d..c4488f1b 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ if (isDev) { if (devEnv.MetadataPath) process.env.METADATA_PATH = devEnv.MetadataPath if (devEnv.FFmpegPath) process.env.FFMPEG_PATH = devEnv.FFmpegPath if (devEnv.FFProbePath) process.env.FFPROBE_PATH = devEnv.FFProbePath + if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1' process.env.SOURCE = 'local' process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || '' } diff --git a/server/managers/BinaryManager.js b/server/managers/BinaryManager.js index b4121ab8..25eb1ebc 100644 --- a/server/managers/BinaryManager.js +++ b/server/managers/BinaryManager.js @@ -24,7 +24,14 @@ class BinaryManager { } async init() { + // Optional skip binaries check + if (process.env.SKIP_BINARIES_CHECK === '1') { + Logger.info('[BinaryManager] Skipping check for binaries') + return + } + if (this.initialized) return + const missingBinaries = await this.findRequiredBinaries() if (missingBinaries.length == 0) return await this.removeOldBinaries(missingBinaries) From 5fcd23409ab10738359d0b1d6790e5cd78d5aa9e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 14 Mar 2024 16:32:23 -0500 Subject: [PATCH 0446/2145] Update:dev.js in devcontainer to include the SkipBinariesCheck flag #2741 --- .devcontainer/dev.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer/dev.js b/.devcontainer/dev.js index 0d113a3e..b5f6714e 100644 --- a/.devcontainer/dev.js +++ b/.devcontainer/dev.js @@ -5,5 +5,6 @@ module.exports.config = { ConfigPath: Path.resolve('config'), MetadataPath: Path.resolve('metadata'), FFmpegPath: '/usr/bin/ffmpeg', - FFProbePath: '/usr/bin/ffprobe' + FFProbePath: '/usr/bin/ffprobe', + SkipBinariesCheck: false } \ No newline at end of file From a1af672c7c8738eb9ba02f8d680e2159712b0096 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Fri, 15 Mar 2024 08:50:51 +0200 Subject: [PATCH 0447/2145] Add unit test workflow --- .github/workflows/unit-tests.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/unit-tests.yml diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 00000000..4c88c16e --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,30 @@ +name: Unit Tests + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch name' + required: true + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name != 'workflow_dispatch' && github.ref_name || inputs.branch}} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test From 0c612b48368f15e4a65e9ad27017dd375d5adc32 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Fri, 15 Mar 2024 09:51:40 +0200 Subject: [PATCH 0448/2145] Update unit test workflow to include push event --- .github/workflows/unit-tests.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 4c88c16e..24f6398c 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -1,22 +1,23 @@ -name: Unit Tests +name: Run Unit Tests on: workflow_dispatch: inputs: - branch: - description: 'Branch name' + ref: + description: 'Branch/Tag/SHA to test' required: true pull_request: + push: jobs: - build: + run-unit-tests: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: - ref: ${{ github.event_name != 'workflow_dispatch' && github.ref_name || inputs.branch}} + ref: ${{ github.event_name != 'workflow_dispatch' && github.ref_name || inputs.ref}} - name: Set up Node.js uses: actions/setup-node@v4 From 630ece82add1ed101e598ed67d3ee0938a39d7bd Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 15 Mar 2024 14:35:09 -0500 Subject: [PATCH 0449/2145] Fix:Chapter modal scroll to current chapter --- client/components/modals/ChaptersModal.vue | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/components/modals/ChaptersModal.vue b/client/components/modals/ChaptersModal.vue index 2ace9891..4c005639 100644 --- a/client/components/modals/ChaptersModal.vue +++ b/client/components/modals/ChaptersModal.vue @@ -34,11 +34,6 @@ export default { data() { return {} }, - watch: { - value(newVal) { - this.$nextTick(this.scrollToChapter) - } - }, computed: { show: { get() { @@ -53,7 +48,7 @@ export default { return this.playbackRate }, currentChapterId() { - return this.currentChapter ? this.currentChapter.id : null + return this.currentChapter?.id || null }, currentChapterStart() { return (this.currentChapter?.start || 0) / this._playbackRate @@ -74,6 +69,11 @@ export default { } } } + }, + updated() { + if (this.value) { + this.$nextTick(this.scrollToChapter) + } } } </script> From 88f9533b37eed2e5fe97f4c7d23598ca8c3cdcad Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 15 Mar 2024 17:10:43 -0500 Subject: [PATCH 0450/2145] Fix:HLS.js retry fragments #2748 #2720 --- client/package-lock.json | 10 ++++---- client/package.json | 4 ++-- client/players/LocalAudioPlayer.js | 37 +++++++++++++++++++++++++----- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index a15762dc..9d513691 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,7 +16,7 @@ "cron-parser": "^4.7.1", "date-fns": "^2.25.0", "epubjs": "^0.3.88", - "hls.js": "^1.0.7", + "hls.js": "^1.5.7", "libarchive.js": "^1.3.0", "nuxt": "^2.17.3", "nuxt-socket-io": "^1.1.18", @@ -8627,9 +8627,9 @@ } }, "node_modules/hls.js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.1.tgz", - "integrity": "sha512-SsUSlpyjOGnwBhVrVEG6vRFPU2SAJ0gUqrFdGeo7YPbOC0vuWK0TDMyp7n3QiaBC/Wkic771uqPnnVdT8/x+3Q==" + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.7.tgz", + "integrity": "sha512-Hnyf7ojTBtXHeOW1/t6wCBJSiK1WpoKF9yg7juxldDx8u3iswrkPt2wbOA/1NiwU4j27DSIVoIEJRAhcdMef/A==" }, "node_modules/hmac-drbg": { "version": "1.0.1", @@ -16976,4 +16976,4 @@ } } } -} \ No newline at end of file +} diff --git a/client/package.json b/client/package.json index d9f11c99..55acee1b 100644 --- a/client/package.json +++ b/client/package.json @@ -21,7 +21,7 @@ "cron-parser": "^4.7.1", "date-fns": "^2.25.0", "epubjs": "^0.3.88", - "hls.js": "^1.0.7", + "hls.js": "^1.5.7", "libarchive.js": "^1.3.0", "nuxt": "^2.17.3", "nuxt-socket-io": "^1.1.18", @@ -36,4 +36,4 @@ "postcss": "^8.3.6", "tailwindcss": "^3.4.1" } -} \ No newline at end of file +} diff --git a/client/players/LocalAudioPlayer.js b/client/players/LocalAudioPlayer.js index 587a52c0..eb1484bb 100644 --- a/client/players/LocalAudioPlayer.js +++ b/client/players/LocalAudioPlayer.js @@ -139,11 +139,30 @@ export default class LocalAudioPlayer extends EventEmitter { } var hlsOptions = { - startPosition: this.startTime || -1 - // No longer needed because token is put in a query string - // xhrSetup: (xhr) => { - // xhr.setRequestHeader('Authorization', `Bearer ${this.token}`) - // } + startPosition: this.startTime || -1, + fragLoadPolicy: { + default: { + maxTimeToFirstByteMs: 10000, + maxLoadTimeMs: 120000, + timeoutRetry: { + maxNumRetry: 4, + retryDelayMs: 0, + maxRetryDelayMs: 0, + }, + errorRetry: { + maxNumRetry: 8, + retryDelayMs: 1000, + maxRetryDelayMs: 8000, + shouldRetry: (retryConfig, retryCount, isTimeout, httpStatus, retry) => { + if (httpStatus?.code === 404 && retryConfig?.maxNumRetry > retryCount) { + console.log(`[HLS] Server 404 for fragment retry ${retryCount} of ${retryConfig.maxNumRetry}`) + return true + } + return retry + } + }, + } + } } this.hlsInstance = new Hls(hlsOptions) @@ -156,9 +175,15 @@ export default class LocalAudioPlayer extends EventEmitter { }) this.hlsInstance.on(Hls.Events.ERROR, (e, data) => { - console.error('[HLS] Error', data.type, data.details, data) if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) { console.error('[HLS] BUFFER STALLED ERROR') + } else if (data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR) { + // Only show error if the fragment is not being retried + if (data.errorAction?.action !== 5) { + console.error('[HLS] FRAG LOAD ERROR', data) + } + } else { + console.error('[HLS] Error', data.type, data.details, data) } }) this.hlsInstance.on(Hls.Events.DESTROYING, () => { From 9e3b3f3e129fcabea023d34a048d840d9c229024 Mon Sep 17 00:00:00 2001 From: dor <dor@raananos.com> Date: Sat, 16 Mar 2024 19:26:22 +0200 Subject: [PATCH 0451/2145] add Hebrew translation json and Hebrew to i18n.js --- client/plugins/i18n.js | 2 + client/strings/he.json | 781 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 783 insertions(+) create mode 100644 client/strings/he.json diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 246ffebd..e35b7e7c 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -12,6 +12,7 @@ const languageCodeMap = { 'es': { label: 'Español', dateFnsLocale: 'es' }, 'et': { label: 'Eesti', dateFnsLocale: 'et' }, 'fr': { label: 'Français', dateFnsLocale: 'fr' }, + 'he': { label: 'עברית', dateFnsLocale: 'he' }, 'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' }, 'it': { label: 'Italiano', dateFnsLocale: 'it' }, 'lt': { label: 'Lietuvių', dateFnsLocale: 'lt' }, @@ -25,6 +26,7 @@ const languageCodeMap = { 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, 'zh-tw': { label: '正體中文 (Traditional Chinese)', dateFnsLocale: 'zhTW' }, + 'he': { label: 'עברית', dateFnsLocale: 'he' } } Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { return { diff --git a/client/strings/he.json b/client/strings/he.json new file mode 100644 index 00000000..582663c6 --- /dev/null +++ b/client/strings/he.json @@ -0,0 +1,781 @@ +{ + "ButtonAdd": "הוסף", + "ButtonAddChapters": "הוסף פרקים", + "ButtonAddDevice": "הוסף התקן", + "ButtonAddLibrary": "הוסף ספרייה", + "ButtonAddPodcasts": "הוסף פודקאסטים", + "ButtonAddUser": "הוסף משתמש", + "ButtonAddYourFirstLibrary": "הוסף את הספרייה הראשונה שלך", + "ButtonApply": "החל", + "ButtonApplyChapters": "החל פרקים", + "ButtonAuthors": "יוצרים", + "ButtonBrowseForFolder": "עיין בתיקייה", + "ButtonCancel": "בטל", + "ButtonCancelEncode": "בטל הצפנה", + "ButtonChangeRootPassword": "שנה סיסמת שורש", + "ButtonCheckAndDownloadNewEpisodes": "בדוק והורד פרקים חדשים", + "ButtonChooseAFolder": "בחר תיקייה", + "ButtonChooseFiles": "בחר קבצים", + "ButtonClearFilter": "נקה פילטר", + "ButtonCloseFeed": "סגור פיד", + "ButtonCollections": "אוספים", + "ButtonConfigureScanner": "הגדר סורק", + "ButtonCreate": "צור", + "ButtonCreateBackup": "צור גיבוי", + "ButtonDelete": "מחק", + "ButtonDownloadQueue": "תור הורדה", + "ButtonEdit": "ערוך", + "ButtonEditChapters": "ערוך פרקים", + "ButtonEditPodcast": "ערוך פודקאסט", + "ButtonForceReScan": "סרוק מחדש בכוח", + "ButtonFullPath": "נתיב מלא", + "ButtonHide": "הסתר", + "ButtonHome": "בית", + "ButtonIssues": "בעיות", + "ButtonJumpBackward": "דלג אחורה", + "ButtonJumpForward": "דלג קדימה", + "ButtonLatest": "אחרון", + "ButtonLibrary": "ספרייה", + "ButtonLogout": "התנתק", + "ButtonLookup": "חפש", + "ButtonManageTracks": "נהל רצועות", + "ButtonMapChapterTitles": "מפה של כותרות פרק", + "ButtonMatchAllAuthors": "התאם את כל היוצרים", + "ButtonMatchBooks": "התאם ספרים", + "ButtonNevermind": "אל תדאג", + "ButtonNext": "הבא", + "ButtonNextChapter": "פרק הבא", + "ButtonOk": "אישור", + "ButtonOpenFeed": "פתח פיד", + "ButtonOpenManager": "פתח מנהל", + "ButtonPause": "השהה", + "ButtonPlay": "נגן", + "ButtonPlaying": "מנגן", + "ButtonPlaylists": "רשימות השמעה", + "ButtonPrevious": "קודם", + "ButtonPreviousChapter": "פרק קודם", + "ButtonPurgeAllCache": "נקה את כל המטמון", + "ButtonPurgeItemsCache": "נקה את מטמון הפריטים", + "ButtonPurgeMediaProgress": "נקה את ההתקדמות במדיה", + "ButtonQueueAddItem": "הוסף לתור", + "ButtonQueueRemoveItem": "הסר מהתור", + "ButtonQuickMatch": "התאם מהר", + "ButtonRead": "קרא", + "ButtonRefresh": "רענן", + "ButtonRemove": "הסר", + "ButtonRemoveAll": "הסר הכל", + "ButtonRemoveAllLibraryItems": "הסר את כל פריטי הספרייה", + "ButtonRemoveFromContinueListening": "הסר מההמשך להאזנה", + "ButtonRemoveFromContinueReading": "הסר מההמשך לקריאה", + "ButtonRemoveSeriesFromContinueSeries": "הסר סדרה מהמשך לסדרות", + "ButtonReScan": "סרוק מחדש", + "ButtonReset": "איפוס", + "ButtonResetToDefault": "איפוס לברירת המחדל", + "ButtonRestore": "שחזר", + "ButtonSave": "שמור", + "ButtonSaveAndClose": "שמור וסגור", + "ButtonSaveTracklist": "שמור רשימת רצועות", + "ButtonScan": "סרוק", + "ButtonScanLibrary": "סרוק ספרייה", + "ButtonSearch": "חפש", + "ButtonSelectFolderPath": "בחר נתיב לתיקייה", + "ButtonSeries": "סדרה", + "ButtonSetChaptersFromTracks": "קבע פרקים מרצועות", + "ButtonShare": "שתף", + "ButtonShiftTimes": "הזז זמנים", + "ButtonShow": "הצג", + "ButtonStartM4BEncode": "התחל הצפנה M4B", + "ButtonStartMetadataEmbed": "התחל הטמעת מטאדאטה", + "ButtonSubmit": "שלח", + "ButtonTest": "בדיקה", + "ButtonUpload": "העלה", + "ButtonUploadBackup": "העלה גיבוי", + "ButtonUploadCover": "העלה כריכה", + "ButtonUploadOPMLFile": "העלה קובץ OPML", + "ButtonUserDelete": "מחק משתמש {0}", + "ButtonUserEdit": "ערוך משתמש {0}", + "ButtonViewAll": "הצג הכול", + "ButtonYes": "כן", + "ErrorUploadFetchMetadataAPI": "שגיאה בשליפת מטאדאטה", + "ErrorUploadFetchMetadataNoResults": "לא ניתן לשלוף מטאדאטה - נסה לעדכן כותרת ו/או יוצר", + "ErrorUploadLacksTitle": "חייב להיות כותרת", + "HeaderAccount": "חשבון", + "HeaderAdvanced": "מתקדם", + "HeaderAppriseNotificationSettings": "הגדרות התראה ב-Apprise", + "HeaderAudiobookTools": "כלים לניהול קבצי אודיו", + "HeaderAudioTracks": "רצועות אודיו", + "HeaderAuthentication": "אימות", + "HeaderBackups": "גיבויים", + "HeaderChangePassword": "שנה סיסמה", + "HeaderChapters": "פרקים", + "HeaderChooseAFolder": "בחר תיקייה", + "HeaderCollection": "אוסף", + "HeaderCollectionItems": "פריטי אוסף", + "HeaderCover": "כריכה", + "HeaderCurrentDownloads": "הורדות נוכחיות", + "HeaderCustomMetadataProviders": "ספקי מטאדאטה מותאמים", + "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": "התראות", + "HeaderOpenIDConnectAuthentication": "אימות OpenID Connect", + "HeaderOpenRSSFeed": "פתח פיד RSS", + "HeaderOtherFiles": "קבצים אחרים", + "HeaderPasswordAuthentication": "אימות סיסמה", + "HeaderPermissions": "הרשאות", + "HeaderPlayerQueue": "תור ניגון", + "HeaderPlaylist": "רשימת השמעה", + "HeaderPlaylistItems": "פריטי רשימת השמעה", + "HeaderPodcastsToAdd": "פודקאסטים להוסיף", + "HeaderPreviewCover": "תצוגה מקדימה של כריכה", + "HeaderRemoveEpisode": "הסר פרק", + "HeaderRemoveEpisodes": "הסר {0} פרקים", + "HeaderRSSFeedGeneral": "פיד RSS כללי", + "HeaderRSSFeedIsOpen": "הפיד RSS פתוח", + "HeaderRSSFeeds": "פידי RSS", + "HeaderSavedMediaProgress": "התקדמות מדיה שמורה", + "HeaderSchedule": "מתזמן", + "HeaderScheduleLibraryScans": "קבע סריקות ספרייה אוטומטיות", + "HeaderSession": "הפעלה", + "HeaderSetBackupSchedule": "קבע מתז גיבוי", + "HeaderSettings": "הגדרות", + "HeaderSettingsDisplay": "הצגה", + "HeaderSettingsExperimental": "תכונות ניסיוניות", + "HeaderSettingsGeneral": "כללי", + "HeaderSettingsScanner": "סורק", + "HeaderSleepTimer": "טיימר שינה", + "HeaderStatsLargestItems": "הפריטים הגדולים ביותר", + "HeaderStatsLongestItems": "הפריטים הארוכים ביותר (בשעות)", + "HeaderStatsMinutesListeningChart": "דקות האזנה (בימים האחרונים)", + "HeaderStatsRecentSessions": "הפעלות אחרונות", + "HeaderStatsTop10Authors": "היוצרים המובילים 10", + "HeaderStatsTop5Genres": "הז'אנרים המובילים 5", + "HeaderTableOfContents": "תוכן העניינים", + "HeaderTools": "כלים", + "HeaderUpdateAccount": "עדכן חשבון", + "HeaderUpdateAuthor": "עדכן יוצר", + "HeaderUpdateDetails": "עדכן פרטים", + "HeaderUpdateLibrary": "עדכן ספרייה", + "HeaderUsers": "משתמשים", + "HeaderYearReview": "שנת {0} בסקירה", + "HeaderYourStats": "הסטטיסטיקות שלך", + "LabelAbridged": "מקוצר", + "LabelAccountType": "סוג חשבון", + "LabelAccountTypeAdmin": "מנהל", + "LabelAccountTypeGuest": "אורח", + "LabelAccountTypeUser": "משתמש", + "LabelActivity": "פעילות", + "LabelAdded": "נוסף", + "LabelAddedAt": "נוסף בתאריך", + "LabelAddToCollection": "הוסף לאוסף", + "LabelAddToCollectionBatch": "הוסף {0} ספרים לאוסף", + "LabelAddToPlaylist": "הוסף לרשימת השמעה", + "LabelAddToPlaylistBatch": "הוסף {0} פריטים לרשימת השמעה", + "LabelAdminUsersOnly": "רק מנהלים", + "LabelAll": "הכל", + "LabelAllUsers": "כל המשתמשים", + "LabelAllUsersExcludingGuests": "כל המשתמשים ללא אורחים", + "LabelAllUsersIncludingGuests": "כל המשתמשים כולל אורחים", + "LabelAlreadyInYourLibrary": "כבר בספרייה שלך", + "LabelAppend": "הוסף לסוף", + "LabelAuthor": "יוצר", + "LabelAuthorFirstLast": "יוצר (שם פרטי שם משפחה)", + "LabelAuthorLastFirst": "יוצר (שם משפחה, שם פרטי)", + "LabelAuthors": "יוצרים", + "LabelAutoDownloadEpisodes": "הורד פרקים באופן אוטומטי", + "LabelAutoFetchMetadata": "שלף מטאדאטה באופן אוטומטי", + "LabelAutoFetchMetadataHelp": "משיג מטאדאטה עבור כותרת, יוצר וסדרה כדי לשפר את תהליך ההעלאה. ייתכן שיהיה על יכולתך להתאים מטאדאטה נוספת לאחר ההעלאה.", + "LabelAutoLaunch": "הפעלה אוטומטית", + "LabelAutoLaunchDescription": "הפניה אוטומטית לספק האימות כאשר מגיעים לדף ההתחברות (ניתן להפעיל ידנית במסלול <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "הרשמה אוטומטית", + "LabelAutoRegisterDescription": "יצירת משתמשים חדשים אוטומטית לאחר התחברות", + "LabelBackToUser": "חזרה למשתמש", + "LabelBackupLocation": "מיקום גיבוי", + "LabelBackupsEnableAutomaticBackups": "הפעל גיבויים אוטומטיים", + "LabelBackupsEnableAutomaticBackupsHelp": "גיבויים שמורים ב /metadata/backups", + "LabelBackupsMaxBackupSize": "גודל הגיבוי המרבי (בג'יגה-בייט)", + "LabelBackupsMaxBackupSizeHelp": "כהגנה על עצמך מפני תצורה שגויה, הגיבויים ייכשלו אם הם יעברו את הגודל שהוגדר.", + "LabelBackupsNumberToKeep": "מספר הגיבויים לשמירה", + "LabelBackupsNumberToKeepHelp": "יש להסיר רק גיבוי אחד בכל פעם, לכן אם יש לך כבר יותר מגיבוי אחד יש להסיר אותם באופן ידני.", + "LabelBitrate": "ביטרייט", + "LabelBooks": "ספרים", + "LabelButtonText": "טקסט לחצן", + "LabelChangePassword": "שינוי סיסמה", + "LabelChannels": "ערוצים", + "LabelChapters": "פרקים", + "LabelChaptersFound": "פרקים נמצאו", + "LabelChapterTitle": "כותרת הפרק", + "LabelClickForMoreInfo": "לחץ למידע נוסף", + "LabelClosePlayer": "סגור נגן", + "LabelCodec": "קודק", + "LabelCollapseSeries": "צמצום סדרה", + "LabelCollection": "אוסף", + "LabelCollections": "אוספים", + "LabelComplete": "מלא", + "LabelConfirmPassword": "אישור סיסמה", + "LabelContinueListening": "המשך האזנה", + "LabelContinueReading": "המשך קריאה", + "LabelContinueSeries": "המשך סדרה", + "LabelCover": "כיסוי", + "LabelCoverImageURL": "כתובת התמונה המצויה ברשת", + "LabelCreatedAt": "נוצר בתאריך", + "LabelCronExpression": "ביטוי קרון", + "LabelCurrent": "נוכחי", + "LabelCurrently": "כעת:", + "LabelCustomCronExpression": "ביטוי קרון מותאם אישית:", + "LabelDatetime": "תאריך ושעה", + "LabelDeleteFromFileSystemCheckbox": "מחיקה מהמערכת הקבצים (הסר סימון למחיקה רק מהמסד נתונים)", + "LabelDescription": "תיאור", + "LabelDeselectAll": "הסר בחירת כל הפריטים", + "LabelDevice": "התקן", + "LabelDeviceInfo": "מידע על התקן", + "LabelDeviceIsAvailableTo": "התקן זמין ל...", + "LabelDirectory": "תיקייה", + "LabelDiscFromFilename": "דיסק משם הקובץ", + "LabelDiscFromMetadata": "דיסק מהמטא-נתונים", + "LabelDiscover": "גלה", + "LabelDownload": "הורד", + "LabelDownloadNEpisodes": "הורד {0} פרקים", + "LabelDuration": "משך", + "LabelDurationFound": "משך נמצא:", + "LabelEbook": "ספר אלקטרוני", + "LabelEbooks": "ספרים אלקטרוניים", + "LabelEdit": "עריכה", + "LabelEmail": "דואר אלקטרוני", + "LabelEmailSettingsFromAddress": "כתובת מאיתה", + "LabelEmailSettingsSecure": "מאובטח", + "LabelEmailSettingsSecureHelp": "אם נכון, החיבור ישתמש ב-TLS בעת התחברות לשרת. אם לא, תישתמש חיבור זה ב-TLS אם השרת תומך בהרחבת STARTTLS. ברוב המקרים עדיף להגדיר ערך זה כנכון אם אתה מחבר לפורט 465. לפורט 587 או 25 שמור על ערך זה כשקר.", + "LabelEmailSettingsTestAddress": "כתובת לבדיקת מבנה", + "LabelEmbeddedCover": "כיסוי משובץ", + "LabelEnable": "הפעל", + "LabelEnd": "סיום", + "LabelEpisode": "פרק", + "LabelEpisodeTitle": "כותרת הפרק", + "LabelEpisodeType": "סוג הפרק", + "LabelExample": "דוגמה", + "LabelExplicit": "ברור", + "LabelFeedURL": "כתובת ערוץ", + "LabelFetchingMetadata": "מושכים מטא-נתונים", + "LabelFile": "קובץ", + "LabelFileBirthtime": "זמן הולדת הקובץ", + "LabelFileModified": "הקובץ הוחלף", + "LabelFilename": "שם הקובץ", + "LabelFilterByUser": "סינון לפי משתמש", + "LabelFindEpisodes": "מצא פרקים", + "LabelFinished": "סיים", + "LabelFolder": "תיקייה", + "LabelFolders": "תיקיות", + "LabelFontBold": "מודגש", + "LabelFontFamily": "משפחת הפונטים", + "LabelFontItalic": "נטוי", + "LabelFontScale": "קנה מידה של הפונט", + "LabelFontStrikethrough": "קו חוצה", + "LabelFormat": "תבנית", + "LabelGenre": "ז'אנר", + "LabelGenres": "ז'אנרים", + "LabelHardDeleteFile": "מחיקה קשה של הקובץ", + "LabelHasEbook": "יש ספר אלקטרוני", + "LabelHasSupplementaryEbook": "יש ספר אלקטרוני תוספתי", + "LabelHighestPriority": "עדיפות הגבוהה ביותר", + "LabelHost": "מארח", + "LabelHour": "שעה", + "LabelIcon": "סמל", + "LabelImageURLFromTheWeb": "כתובת התמונה מהרשת", + "LabelIncludeInTracklist": "כלול ברשימת השמעה", + "LabelIncomplete": "לא הושלם", + "LabelInProgress": "בתהליך", + "LabelInterval": "מרווח", + "LabelIntervalCustomDailyWeekly": "מותאם אישית יומי/שבועי", + "LabelIntervalEvery12Hours": "כל 12 שעות", + "LabelIntervalEvery15Minutes": "כל 15 דקות", + "LabelIntervalEvery2Hours": "כל 2 שעות", + "LabelIntervalEvery30Minutes": "כל 30 דקות", + "LabelIntervalEvery6Hours": "כל 6 שעות", + "LabelIntervalEveryDay": "כל יום", + "LabelIntervalEveryHour": "כל שעה", + "LabelInvalidParts": "חלקים לא תקינים", + "LabelInvert": "הפוך", + "LabelItem": "פריט", + "LabelLanguage": "שפה", + "LabelLanguageDefaultServer": "שפת השרת ברירת המחדל", + "LabelLastBookAdded": "הספר האחרון שהוסף", + "LabelLastBookUpdated": "הספר האחרון שעודכן", + "LabelLastSeen": "נראה לאחרונה", + "LabelLastTime": "הפעם האחרונה", + "LabelLastUpdate": "עדכון אחרון", + "LabelLayout": "פריסה", + "LabelLayoutSinglePage": "דף בודד", + "LabelLayoutSplitPage": "פיצול הדף", + "LabelLess": "פחות", + "LabelLibrariesAccessibleToUser": "ספריות נגישות למשתמש", + "LabelLibrary": "ספרייה", + "LabelLibraryItem": "פריט ספרייה", + "LabelLibraryName": "שם הספרייה", + "LabelLimit": "מגבלה", + "LabelLineSpacing": "רווח שורות", + "LabelListenAgain": "האזן שוב", + "LabelLogLevelDebug": "דיבוג", + "LabelLogLevelInfo": "מידע", + "LabelLogLevelWarn": "אזהרה", + "LabelLookForNewEpisodesAfterDate": "חפש פרקים חדשים לאחר תאריך זה", + "LabelLowestPriority": "עדיפות הנמוכה ביותר", + "LabelMatchExistingUsersBy": "התאם משתמשים קיימים לפי", + "LabelMatchExistingUsersByDescription": "משמש לחיבור משתמשים קיימים. לאחר החיבור, המשתמשים יתאמו לפי זיהוי ייחודי מספק ה-SSO שלך", + "LabelMediaPlayer": "נגן מדיה", + "LabelMediaType": "סוג מדיה", + "LabelMetadataOrderOfPrecedenceDescription": "מקורות המטא-נתונים עם עדיפות גבוהה יחליפו מקורות עם עדיפות נמוכה יותר", + "LabelMetadataProvider": "ספק מטא-נתונים", + "LabelMetaTag": "תג מטא", + "LabelMetaTags": "תגי מטא", + "LabelMinute": "דקה", + "LabelMissing": "חסר", + "LabelMissingEbook": "אין ספר אלקטרוני", + "LabelMissingParts": "חלקים חסרים", + "LabelMissingSupplementaryEbook": "אין ספר אלקטרוני נלווה", + "LabelMobileRedirectURIs": "כתובות משדר ניידות מורשות", + "LabelMobileRedirectURIsDescription": "זהו רשימה לבניה של כתובות ה-URI הנתמכות להפניות עבור אפליקציות ניידות. הברירת מחדל היא <code>audiobookshelf://oauth</code>, שניתן להסיר או להוסיף לה כתובות נוספות לאינטגרציה עם אפליקציות צד שלישי. שימוש בכוכבית (<code>*</code>) כקלט בודד מאפשר כל URI.", + "LabelMore": "עוד", + "LabelMoreInfo": "מידע נוסף", + "LabelName": "שם", + "LabelNarrator": "נרטור", + "LabelNarrators": "נרטורים", + "LabelNew": "חדש", + "LabelNewestAuthors": "סופרים החדשים ביותר", + "LabelNewestEpisodes": "הפרקים החדשים ביותר", + "LabelNewPassword": "סיסמה חדשה", + "LabelNextBackupDate": "תאריך גיבוי הבא", + "LabelNextScheduledRun": "הרצה מתוזמנת הבאה", + "LabelNoEpisodesSelected": "לא נבחרו פרקים", + "LabelNotes": "הערות", + "LabelNotFinished": "לא הושלם", + "LabelNotificationAppriseURL": "כתובת URL של התראה", + "LabelNotificationAvailableVariables": "משתנים זמינים", + "LabelNotificationBodyTemplate": "תבנית גוף", + "LabelNotificationEvent": "אירוע התראה", + "LabelNotificationsMaxFailedAttempts": "מספר הניסיונות הנכשלים המרבי", + "LabelNotificationsMaxFailedAttemptsHelp": "ההתראות מושבתות לאחר שהן נכשלות לשלוח מספר זה פעמים", + "LabelNotificationsMaxQueueSize": "גודל התור המרבי לאירועי התראה", + "LabelNotificationsMaxQueueSizeHelp": "האירועים מוגבלים לשליחה אחת לשנייה. האירועים יתעלמו אם התור מלא בגודלו המרבי. זה מונע ספאם בהתראות.", + "LabelNotificationTitleTemplate": "תבנית כותרת", + "LabelNotStarted": "לא התחיל", + "LabelNumberOfBooks": "מספר הספרים", + "LabelNumberOfEpisodes": "מספר הפרקים", + "LabelOpenRSSFeed": "פתח ערוץ RSS", + "LabelOverwrite": "לשכפל", + "LabelPassword": "סיסמה", + "LabelPath": "נתיב", + "LabelPermissionsAccessAllLibraries": "ניתן לגשת לכל הספריות", + "LabelPermissionsAccessAllTags": "ניתן לגשת לכל התגיות", + "LabelPermissionsAccessExplicitContent": "ניתן לגשת לתוכן מפורט", + "LabelPermissionsDelete": "ניתן למחוק", + "LabelPermissionsDownload": "ניתן להוריד", + "LabelPermissionsUpdate": "ניתן לעדכן", + "LabelPermissionsUpload": "ניתן להעלות", + "LabelPersonalYearReview": "השנה שלך בסקירה ({0})", + "LabelPhotoPathURL": "נתיב/URL לתמונה", + "LabelPlaylists": "רשימות השמעה", + "LabelPlayMethod": "שיטת הפעלה", + "LabelPodcast": "פודקאסט", + "LabelPodcasts": "פודקאסטים", + "LabelPodcastSearchRegion": "אזור חיפוש פודקאסט", + "LabelPodcastType": "סוג פודקאסט", + "LabelPort": "יציאה", + "LabelPrefixesToIgnore": "קידומות להתעלמות (קסה אינסנסיטיבית)", + "LabelPreventIndexing": "מנע את האינדוקסציה של הפיד שלך על ידי ספריות אייטונס וגוגל פודקאסט", + "LabelPrimaryEbook": "ספר אלקטרוני ראשי", + "LabelProgress": "התקדמות", + "LabelProvider": "ספק", + "LabelPubDate": "תאריך פרסום", + "LabelPublisher": "מוציא לאור", + "LabelPublishYear": "שנת הפרסום", + "LabelRead": "קריאה", + "LabelReadAgain": "קרא שוב", + "LabelReadEbookWithoutProgress": "קרוא ספר אלקטרוני בלי לשמור התקדמות", + "LabelRecentlyAdded": "נוסף לאחרונה", + "LabelRecentSeries": "סדרות אחרונות", + "LabelRecommended": "מומלץ", + "LabelRedo": "עשה שוב", + "LabelRegion": "אזור", + "LabelReleaseDate": "תאריך שחרור", + "LabelRemoveCover": "הסר כיסוי", + "LabelRowsPerPage": "שורות לעמוד", + "LabelRSSFeedCustomOwnerEmail": "אימייל בעלים מותאם אישית", + "LabelRSSFeedCustomOwnerName": "שם בעלים מותאם אישית", + "LabelRSSFeedOpen": "פתח ערוץ RSS", + "LabelRSSFeedPreventIndexing": "מנע אינדוקסציה", + "LabelRSSFeedSlug": "שם תמצאות RSS", + "LabelRSSFeedURL": "URL של פיד RSS", + "LabelSearchTerm": "מונח חיפוש", + "LabelSearchTitle": "כותרת חיפוש", + "LabelSearchTitleOrASIN": "כותרת חיפוש או ASIN", + "LabelSeason": "עונה", + "LabelSelectAllEpisodes": "בחר את כל הפרקים", + "LabelSelectEpisodesShowing": "בחר {0} פרקים המוצגים", + "LabelSelectUsers": "בחר משתמשים", + "LabelSendEbookToDevice": "שלח ספר אלקטרוני ל...", + "LabelSequence": "רצף", + "LabelSeries": "סדרה", + "LabelSeriesName": "שם הסדרה", + "LabelSeriesProgress": "התקדמות בסדרה", + "LabelServerYearReview": "השנה בסקירה של השרת ({0})", + "LabelSetEbookAsPrimary": "קבע כראשי", + "LabelSetEbookAsSupplementary": "קבע כספר אלקטרוני נלווה", + "LabelSettingsAudiobooksOnly": "רק ספרי קול", + "LabelSettingsAudiobooksOnlyHelp": "הפעלת ההגדרה הזו תתעלם מקבצי ספרים אלקטרוניים אלא אם כן הם נמצאים בתיקיית ספרי קול, שבמקרה זה יקבעו כספרים אלקטרוניים נלווים", + "LabelSettingsBookshelfViewHelp": "עיצוב סקיומורפי עם מדפים עץ", + "LabelSettingsChromecastSupport": "תמיכת Chromecast", + "LabelSettingsDateFormat": "פורמט תאריך", + "LabelSettingsDisableWatcher": "השבת צופה", + "LabelSettingsDisableWatcherForLibrary": "השבת צופה תיקייה עבור ספרייה", + "LabelSettingsDisableWatcherHelp": "מבטל את הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", + "LabelSettingsEnableWatcher": "הפעל צופה", + "LabelSettingsEnableWatcherForLibrary": "הפעל צופה תיקייה עבור ספרייה", + "LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", + "LabelSettingsExperimentalFeatures": "תכונות ניסיוניות", + "LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.", + "LabelSettingsFindCovers": "מצא כיסויים", + "LabelSettingsFindCoversHelp": "אם לספר השמע שלך אין כיסוי מוטמע או תמונת כיסוי בתיקייה, הסורק ינסה למצוא כיסוי.<br>שים לב: זה יאריך את זמן הסריקה", + "LabelSettingsHideSingleBookSeries": "הסתר סדרות ספר אחד", + "LabelSettingsHideSingleBookSeriesHelp": "סדרות הכוללות ספר אחד יוסתרו מדף הסדרות ומדף הבית.", + "LabelSettingsHomePageBookshelfView": "השתמש בתצוגת מדף על דף הבית", + "LabelSettingsLibraryBookshelfView": "השתמש בתצוגת מדף בספרייה", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "דלג על ספרים קודמים בהמשך סדרות", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "מדף המשך סדרות מציג את הספר הראשון שלא התחיל בסדרה שיש לפחות ספר אחד שהושלם ואין ספרים בתהליך. הפעלת ההגדרה הזו תמשיך סדרות מהספר שהושלם הכי הרחק במקום מהספר הראשון שלא התחיל.", + "LabelSettingsParseSubtitles": "פענח כתוביות", + "LabelSettingsParseSubtitlesHelp": "העתק כותרת משנה משם תיקיית הספר.<br>כותרת המשנה חייבת להיות מופרדת עם התו ״-״<br>לדוגמא, כותרת המשנה לספר ״שם הספר - כותרת משנה״, היא ״כותרת משנה״", + "LabelSettingsPreferMatchedMetadata": "עדיף מטה-נתונים מתואמים", + "LabelSettingsPreferMatchedMetadataHelp": "נתונים מתואמים ידריך פרטי פריט כאשר משתמשים בהתאמה מהירה. כברירת מחדל, התאמה מהירה תמלא פרטים חסרים בלבד.", + "LabelSettingsSkipMatchingBooksWithASIN": "דלג על ספרים שכבר יש להם ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "דלג על ספרים שכבר יש להם ISBN", + "LabelSettingsSortingIgnorePrefixes": "התעלם מקידומות במיון", + "LabelSettingsSortingIgnorePrefixesHelp": "לדוגמא, לקידומת ״ה״ שם הספר, שם הספר ימוין בתור ״שם הספר״, ״ה״", + "LabelSettingsSquareBookCovers": "השתמש בכיסויים מרובעים לספרים", + "LabelSettingsSquareBookCoversHelp": "מעדיף להשתמש בכיסויים מרובעים מעל כיסויים סטנדרטיים ביחס 1.6:1", + "LabelSettingsStoreCoversWithItem": "אחסן כיסויים עם פריט", + "LabelSettingsStoreCoversWithItemHelp": "כברירת מחדל, צילומי כריכות נשמרים בתיקיית /metadata/items, לאחר הפעלת הגדרה זו צילומי כריכות יישמרו בתיקיית הספר, רק קובץ אחד בשם ״cover״ יישמר", + "LabelSettingsStoreMetadataWithItem": "אחסן מטה-נתונים עם פריט", + "LabelSettingsStoreMetadataWithItemHelp": "כברירת מחדל, קבצי מטה-נתונים מאוחסנים ב- /metadata/items, הפעלת ההגדרה תאחסן קבצי מטה-נתונים בתיקיית פריט שלך בספרייה", + "LabelSettingsTimeFormat": "פורמט זמן", + "LabelShowAll": "הצג הכל", + "LabelSize": "גודל", + "LabelSleepTimer": "טיימר שינה", + "LabelSlug": "Slug", + "LabelStart": "התחלה", + "LabelStarted": "התחיל", + "LabelStartedAt": "התחיל ב", + "LabelStartTime": "זמן התחלה", + "LabelStatsAudioTracks": "רצועות שמע", + "LabelStatsAuthors": "מחברים", + "LabelStatsBestDay": "היום הטוב ביותר", + "LabelStatsDailyAverage": "ממוצע יומי", + "LabelStatsDays": "ימים", + "LabelStatsDaysListened": "ימים שהוקשבו", + "LabelStatsHours": "שעות", + "LabelStatsInARow": "ברצף", + "LabelStatsItemsFinished": "פריטים שסיימו", + "LabelStatsItemsInLibrary": "פריטים בספרייה", + "LabelStatsMinutes": "דקות", + "LabelStatsMinutesListening": "דקות האזנה", + "LabelStatsOverallDays": "ימים כולל", + "LabelStatsOverallHours": "שעות כולל", + "LabelStatsWeekListening": "האזנה שבועית", + "LabelSubtitle": "תת כותרת", + "LabelSupportedFileTypes": "סוגי קבצים נתמכים", + "LabelTag": "תג", + "LabelTags": "תגיות", + "LabelTagsAccessibleToUser": "תגיות נגישות למשתמש", + "LabelTagsNotAccessibleToUser": "תגיות לא נגישות למשתמש", + "LabelTasks": "משימות פעילות", + "LabelTextEditorBulletedList": "רשימה עם נקודות", + "LabelTextEditorLink": "קישור", + "LabelTextEditorNumberedList": "רשימה ממוספרת", + "LabelTextEditorUnlink": "ביטול קישור", + "LabelTheme": "ערכת נושא", + "LabelThemeDark": "כהה", + "LabelThemeLight": "בהיר", + "LabelTimeBase": "בסיס זמן", + "LabelTimeListened": "זמן האזנה", + "LabelTimeListenedToday": "זמן האזנה היום", + "LabelTimeRemaining": "{0} נותרו", + "LabelTimeToShift": "זמן להיסט בשניות", + "LabelTitle": "כותרת", + "LabelToolsEmbedMetadata": "הטמעת מטה-נתונים", + "LabelToolsEmbedMetadataDescription": "הטמעת מטה-נתונים לקבצי שמע כולל תמונות שער ופרקים.", + "LabelToolsMakeM4b": "יצירת קובץ אודיו M4B", + "LabelToolsMakeM4bDescription": "יצירת קובץ אודיו .M4B עם מטה-נתונים מוטמעים, תמונת שער ופרקים.", + "LabelToolsSplitM4b": "פיצול M4B ל-MP3", + "LabelToolsSplitM4bDescription": "יצירת קבצי MP3 מ-M4B מפוצל לפי פרקים עם מטה-נתונים מוטמעים, תמונת שער ופרקים.", + "LabelTotalDuration": "משך כולל", + "LabelTotalTimeListened": "סך הזמן שהוקשב", + "LabelTrackFromFilename": "מסלול משמות קבצים", + "LabelTrackFromMetadata": "מסלול ממטה-נתונים", + "LabelTracks": "מסלולים", + "LabelTracksMultiTrack": "מסלול רב-ערוצים", + "LabelTracksNone": "אין מסלולים", + "LabelTracksSingleTrack": "מסלול יחיד", + "LabelType": "סוג", + "LabelUnabridged": "לא מקוצר", + "LabelUndo": "בטל", + "LabelUnknown": "לא ידוע", + "LabelUpdateCover": "עדכן כריכה", + "LabelUpdateCoverHelp": "אפשר כיסוי מעלה של כריכות קיימות עבור הספרים הנבחרים כאשר נמצאה התאמה", + "LabelUpdatedAt": "עודכן ב", + "LabelUpdateDetails": "עדכון פרטים", + "LabelUpdateDetailsHelp": "אפשר עדכון מעלה של פרטים קיימים עבור הספרים הנבחרים כאשר נמצאה התאמה", + "LabelUploaderDragAndDrop": "גרור ושחרר קבצים או תיקיות", + "LabelUploaderDropFiles": "שחרר קבצים", + "LabelUploaderItemFetchMetadataHelp": "משיכת כותרת, סופר וסדרה באופן אוטומטי", + "LabelUseChapterTrack": "השתמש במסלול פרקים", + "LabelUseFullTrack": "השתמש במסלול מלא", + "LabelUser": "משתמש", + "LabelUsername": "שם משתמש", + "LabelValue": "ערך", + "LabelVersion": "גרסה", + "LabelViewBookmarks": "הצג סימניות", + "LabelViewChapters": "הצג פרקים", + "LabelViewQueue": "הצג תור הנגן", + "LabelVolume": "עוצמת קול", + "LabelWeekdaysToRun": "ימי השבוע להרצה", + "LabelYearReviewHide": "הסתר שנת סקירה", + "LabelYearReviewShow": "ראה שנת סקירה", + "LabelYourAudiobookDuration": "משך האודיובוק שלך", + "LabelYourBookmarks": "הסימניות שלך", + "LabelYourPlaylists": "הפלייליסטים שלך", + "LabelYourProgress": "ההתקדמות שלך", + "MessageAddToPlayerQueue": "הוסף לתור הנגן", + "MessageAppriseDescription": "כדי להשתמש בתכונה זו יש לך להריץ מופע של <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">ממשק התכנית האפליקציה</a> או API שיטפל בבקשות אלו. <br /> כתובת URL של ממשק ה-Apprise API צריכה להיות הנתיב המלא לשליחת ההתראה, לדוגמה, אם המופע של ה-API שלך מוצע ב-<code>http://192.168.1.1:8337</code> אז עליך לשים <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "גיבויים כוללים משתמשים, התקדמות משתמש, פרטי פריטי ספרייה, הגדרות שרת ותמונות השמורות ב-<code>/metadata/items</code> & <code>/metadata/authors</code>. גיבויים <strong>לא</strong> כוללים קבצים שמורים בתיקיות הספרייה שלך.", + "MessageBatchQuickMatchDescription": "התאמה מהירה תנסה להוסיף כריכות ומטה-נתונים חסרים עבור הפריטים הנבחרים. הפעל את האפשרויות למטה כדי לאפשר להתאמה מהירה לדרוס כריכות קיימות ו/או מטה-נתונים.", + "MessageBookshelfNoCollections": "עדיין לא יצרת שום אוספים", + "MessageBookshelfNoResultsForFilter": "אין תוצאות עבור מסנן \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "אין RSS feeds פתוחים", + "MessageBookshelfNoSeries": "אין לך סדרות", + "MessageChapterEndIsAfter": "סיום הפרק מאוחר מסיום האודיובוק שלך", + "MessageChapterErrorFirstNotZero": "הפרק הראשון חייב להתחיל ב-0", + "MessageChapterErrorStartGteDuration": "זמן התחלה לא תקין חייב להיות פחות ממשך האודיובוק", + "MessageChapterErrorStartLtPrev": "זמן התחלה לא תקין חייב להיות גדול או שווה לזמן התחלה של הפרק הקודם", + "MessageChapterStartIsAfter": "התחלת הפרק מאוחרת מסיום האודיובוק שלך", + "MessageCheckingCron": "בודק את תזמון העבודה...", + "MessageConfirmCloseFeed": "האם אתה בטוח שאתה רוצה לסגור את הפיד הזה?", + "MessageConfirmDeleteBackup": "האם אתה בטוח שברצונך למחוק גיבוי עבור {0}?", + "MessageConfirmDeleteFile": "זה ימחק את הקובץ מהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteLibrary": "האם אתה בטוח שברצונך למחוק לצמיתות את הספרייה \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "זה ימחק את פריט הספרייה ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteLibraryItems": "זה ימחק {0} פריטי ספרייה ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteSession": "האם אתה בטוח שאתה רוצה למחוק את ההפעלה הזו?", + "MessageConfirmForceReScan": "האם אתה בטוח שאתה רוצה לכוון איתור מחדש?", + "MessageConfirmMarkAllEpisodesFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כסיום?", + "MessageConfirmMarkAllEpisodesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כלא סיום?", + "MessageConfirmMarkSeriesFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כסיום?", + "MessageConfirmMarkSeriesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כלא סיום?", + "MessageConfirmQuickEmbed": "אזהרה! הטמעה מהירה לא תגבה גיבוי של קבצי האודיו שלך. וודא שיש לך גיבוי של קבצי האודיו שלך. <br><br>האם ברצונך להמשיך?", + "MessageConfirmRemoveAllChapters": "האם אתה בטוח שברצונך להסיר את כל הפרקים?", + "MessageConfirmRemoveAuthor": "האם אתה בטוח שברצונך להסיר את המחבר \"{0}\"?", + "MessageConfirmRemoveCollection": "האם אתה בטוח שברצונך להסיר אוסף \"{0}\"?", + "MessageConfirmRemoveEpisode": "האם אתה בטוח שברצונך להסיר פרק \"{0}\"?", + "MessageConfirmRemoveEpisodes": "האם אתה בטוח שברצונך להסיר {0} פרקים?", + "MessageConfirmRemoveListeningSessions": "האם אתה בטוח שברצונך להסיר {0} מפגשי האזנה?", + "MessageConfirmRemoveNarrator": "האם אתה בטוח שברצונך להסיר נרטור \"{0}\"?", + "MessageConfirmRemovePlaylist": "האם אתה בטוח שברצונך להסיר את רשימת ההשמעה שלך \"{0}\"?", + "MessageConfirmRenameGenre": "האם אתה בטוח שברצונך לשנות את שם הג'אנר \"{0}\" ל \"{1}\" עבור כל הפריטים?", + "MessageConfirmRenameGenreMergeNote": "הערה: ג'אנר זה כבר קיים ולכן הם יתמזגו.", + "MessageConfirmRenameGenreWarning": "אזהרה! יש ג'אנר דומה עם רישום שונה הכבר קיים \"{0}\".", + "MessageConfirmRenameTag": "האם אתה בטוח שברצונך לשנות את שם התג \"{0}\" ל \"{1}\" עבור כל הפריטים?", + "MessageConfirmRenameTagMergeNote": "הערה: התג זה כבר קיים ולכן הם יתמזגו.", + "MessageConfirmRenameTagWarning": "אזהרה! יש תג דומה עם רישום שונה הכבר קיים \"{0}\".", + "MessageConfirmReScanLibraryItems": "האם אתה בטוח שברצונך לסרוק מחדש {0} פריטים?", + "MessageConfirmSendEbookToDevice": "האם אתה בטוח שברצונך לשלוח {0} איבוק \"{1}\" למכשיר \"{2}\"?", + "MessageDownloadingEpisode": "מוריד פרק", + "MessageDragFilesIntoTrackOrder": "גרור קבצים לסדר השמעה נכון", + "MessageEmbedFinished": "ההטמעה הושלמה!", + "MessageEpisodesQueuedForDownload": "{0} פרקים בתור להורדה", + "MessageFeedURLWillBe": "כתובת URL של העדכון תהיה {0}", + "MessageFetching": "מבצע גישה...", + "MessageForceReScanDescription": "יסרוק את כל הקבצים מחדש כמו סריקה חדשה. תגיות ID3 של קבצי שמע, קבצי OPF וקבצי טקסט יסרקו כחדשים.", + "MessageImportantNotice": "הודעה חשובה!", + "MessageInsertChapterBelow": "הוסף פרק למטה", + "MessageItemsSelected": "{0} פריטים נבחרו", + "MessageItemsUpdated": "{0} פריטים עודכנו", + "MessageJoinUsOn": "הצטרף אלינו ב-", + "MessageListeningSessionsInTheLastYear": "{0} מפגשי האזנה בשנה האחרונה", + "MessageLoading": "טוען...", + "MessageLoadingFolders": "טוען תיקיות...", + "MessageM4BFailed": "M4B נכשל!", + "MessageM4BFinished": "M4B הושלם!", + "MessageMapChapterTitles": "מפה שמות פרקים לפרקי הספר השמורים שלך ללא שינוי תגים זמניים", + "MessageMarkAllEpisodesFinished": "סמן את כל הפרקים כהסתיימו", + "MessageMarkAllEpisodesNotFinished": "סמן את כל הפרקים כלא הסתיימו", + "MessageMarkAsFinished": "סמן כהסתיים", + "MessageMarkAsNotFinished": "סמן כלא הסתיים", + "MessageMatchBooksDescription": "ינסה להתאים ספרים בספריית הספרים שלך עם ספר מספק החיפוש הנבחר וימלא פרטים ריקים ותמונות כריכה. לא ימחק פרטים.", + "MessageNoAudioTracks": "אין רצועות שמע", + "MessageNoAuthors": "אין סופרים", + "MessageNoBackups": "אין גיבויים", + "MessageNoBookmarks": "אין סימניות", + "MessageNoChapters": "אין פרקים", + "MessageNoCollections": "אין אוספים", + "MessageNoCoversFound": "לא נמצאו כריכות", + "MessageNoDescription": "אין תיאור", + "MessageNoDownloadsInProgress": "אין הורדות פעילות כרגע", + "MessageNoDownloadsQueued": "אין הורדות בתור", + "MessageNoEpisodeMatchesFound": "לא נמצאו התאמות לפרק", + "MessageNoEpisodes": "אין פרקים", + "MessageNoFoldersAvailable": "אין תיקיות זמינות", + "MessageNoGenres": "אין ז'אנרים", + "MessageNoIssues": "אין בעיות", + "MessageNoItems": "אין פריטים", + "MessageNoItemsFound": "לא נמצאו פריטים", + "MessageNoListeningSessions": "אין מפגשי האזנה", + "MessageNoLogs": "אין לוגים", + "MessageNoMediaProgress": "אין התקדמות מדיה", + "MessageNoNotifications": "אין התראות", + "MessageNoPodcastsFound": "לא נמצאו פודקאסטים", + "MessageNoResults": "אין תוצאות", + "MessageNoSearchResultsFor": "אין תוצאות חיפוש עבור \"{0}\"", + "MessageNoSeries": "אין סדרות", + "MessageNoTags": "אין תגיות", + "MessageNoTasksRunning": "אין משימות פעילות", + "MessageNotYetImplemented": "עדיין לא מיושם", + "MessageNoUpdateNecessary": "לא נדרש עדכון", + "MessageNoUpdatesWereNecessary": "לא הייתה צורך בעדכונים", + "MessageNoUserPlaylists": "אין לך רשימות השמעה", + "MessageOr": "או", + "MessagePauseChapter": "השהה השמעת הפרק", + "MessagePlayChapter": "השמע לתחילת הפרק", + "MessagePlaylistCreateFromCollection": "צור רשימת השמעה מאוסף", + "MessagePodcastHasNoRSSFeedForMatching": "הפודקאסט אין לו כתובת URL של הזנת RSS לשימוש בהתאמה", + "MessageQuickMatchDescription": "ממלא פרטים ריקים וכריכות עם התוצאה הראשונה מ '{0}'. לא ימחק פרטים אלא אם הגדרות השרת 'מעדיפים מטה-נתונים התואמים' מופעלות.", + "MessageRemoveChapter": "הסר פרק", + "MessageRemoveEpisodes": "הסר {0} פרקים", + "MessageRemoveFromPlayerQueue": "הסר מתור ההשמעה של הנגן", + "MessageRemoveUserWarning": "האם אתה בטוח שברצונך למחוק לצמיתות את המשתמש \"{0}\"?", + "MessageReportBugsAndContribute": "דווח על באגים, בקש תכונות חדשות, ותרום ב-", + "MessageResetChaptersConfirm": "האם אתה בטוח שברצונך לאפס את הפרקים ולבטל את השינויים שביצעת?", + "MessageRestoreBackupConfirm": "האם אתה בטוח שברצונך לשחזר את הגיבוי שנוצר ב", + "MessageRestoreBackupWarning": "שחזור גיבוי ימחק את כל מסד הנתונים השוכן ב /config ואת תמונות הכריכה ב- /metadata/items & /metadata/authors.<br /><br />גיבויים אינם משנים קבצים בתיקיות הספרייה שלך. אם הגדרות השרת מופעלות כדי לאחסן תמונות כריכה ומטאדאטה בתיקיות הספרייה שלך אז אלה לא יגובו או ימחקו.<br /><br />כל הלקוחות המשתמשים בשרת שלך יתעדכנו באופן אוטומטי.", + "MessageSearchResultsFor": "תוצאות חיפוש עבור", + "MessageSelected": "{0} נבחרו", + "MessageServerCouldNotBeReached": "לא ניתן להגיע אל השרת", + "MessageSetChaptersFromTracksDescription": "קבע פרקים באמצעות כל קובץ שמע כפרק וכותרת פרק כשם הקובץ שמע", + "MessageStartPlaybackAtTime": "האם אתה בטוח שברצונך להתחיל השמעה עבור \"{0}\" ב-{1}?", + "MessageThinking": "חושב...", + "MessageUploaderItemFailed": "העלאת הפריט נכשלה", + "MessageUploaderItemSuccess": "העלאה הצליחה!", + "MessageUploading": "מעלה...", + "MessageValidCronExpression": "ביטוי Cron חוקי", + "MessageWatcherIsDisabledGlobally": "שומר מנוטרל באופן גלובלי בהגדרות השרת", + "MessageXLibraryIsEmpty": "ספריית {0} ריקה!", + "MessageYourAudiobookDurationIsLonger": "הזמן של הספר השמע שלך ארוך יותר מהזמן שנמצא", + "MessageYourAudiobookDurationIsShorter": "הזמן של הספר השמע שלך קצר יותר מהזמן שנמצא", + "NoteChangeRootPassword": "המשתמש השורש הוא המשתמש היחיד שיכול להיות לו סיסמה ריקה", + "NoteChapterEditorTimes": "הערה: זמן ההתחלה של הפרק הראשון חייב להישאר 0:00 וזמן ההתחלה של הפרק האחרון לא יכול לחרוג מהזמן של ספר השמע הזה.", + "NoteFolderPicker": "הערה: תיקיות שכבר מוגדרות לא יוצגו", + "NoteRSSFeedPodcastAppsHttps": "אזהרה: רוב יישומי הפודקאסט דורשים שהכתובת URL של העדכון תשתמש ב HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "אזהרה: פרק(ים) אחד או יותר לא מכילים תאריך פרסום. חלק מיישומי הפודקאסט דורשים זאת.", + "NoteUploaderFoldersWithMediaFiles": "תיקיות עם קבצי מדיה יעובדו כפריטי ספריה נפרדים.", + "NoteUploaderOnlyAudioFiles": "אם מעלים רק קבצי שמע, כל קובץ שמע יעובד כספר שמע נפרד.", + "NoteUploaderUnsupportedFiles": "קבצים לא נתמכים מתעלמים. בעת בחירת תיקייה או זריקתה, קבצים אחרים שאינם בתיקיית פריט מתעלמים.", + "PlaceholderNewCollection": "שם אוסף חדש", + "PlaceholderNewFolderPath": "נתיב תיקייה חדשה", + "PlaceholderNewPlaylist": "שם רשימת השמעה חדשה", + "PlaceholderSearch": "חיפוש..", + "PlaceholderSearchEpisode": "חיפוש פרק..", + "ToastAccountUpdateFailed": "עדכון חשבון נכשל", + "ToastAccountUpdateSuccess": "חשבון עודכן בהצלחה", + "ToastAuthorImageRemoveFailed": "הסרת התמונה של המחבר נכשלה", + "ToastAuthorImageRemoveSuccess": "תמונת המחבר הוסרה בהצלחה", + "ToastAuthorUpdateFailed": "עדכון המחבר נכשל", + "ToastAuthorUpdateMerged": "המחבר ממוזג", + "ToastAuthorUpdateSuccess": "המחבר עודכן בהצלחה", + "ToastAuthorUpdateSuccessNoImageFound": "המחבר עודכן (תמונה לא נמצאה)", + "ToastBackupCreateFailed": "יצירת גיבוי נכשלה", + "ToastBackupCreateSuccess": "גיבוי נוצר בהצלחה", + "ToastBackupDeleteFailed": "מחיקת הגיבוי נכשלה", + "ToastBackupDeleteSuccess": "הגיבוי נמחק בהצלחה", + "ToastBackupRestoreFailed": "שחזור הגיבוי נכשל", + "ToastBackupUploadFailed": "העלאת הגיבוי נכשלה", + "ToastBackupUploadSuccess": "הגיבוי הועלה בהצלחה", + "ToastBatchUpdateFailed": "עדכון הצטברות נכשל", + "ToastBatchUpdateSuccess": "הצטברות עודכנה בהצלחה", + "ToastBookmarkCreateFailed": "יצירת סימניה נכשלה", + "ToastBookmarkCreateSuccess": "הסימניה נוספה בהצלחה", + "ToastBookmarkRemoveFailed": "הסרת הסימניה נכשלה", + "ToastBookmarkRemoveSuccess": "הסימניה הוסרה בהצלחה", + "ToastBookmarkUpdateFailed": "עדכון הסימניה נכשל", + "ToastBookmarkUpdateSuccess": "הסימניה עודכנה בהצלחה", + "ToastChaptersHaveErrors": "הפרקים מכילים שגיאות", + "ToastChaptersMustHaveTitles": "הפרקים חייבים לכלול כותרות", + "ToastCollectionItemsRemoveFailed": "הסרת הפריט(ים) מהאוסף נכשלה", + "ToastCollectionItemsRemoveSuccess": "הפריט(ים) הוסרו מהאוסף בהצלחה", + "ToastCollectionRemoveFailed": "מחיקת האוסף נכשלה", + "ToastCollectionRemoveSuccess": "האוסף הוסר בהצלחה", + "ToastCollectionUpdateFailed": "עדכון האוסף נכשל", + "ToastCollectionUpdateSuccess": "האוסף עודכן בהצלחה", + "ToastItemCoverUpdateFailed": "עדכון כיסוי הפריט נכשל", + "ToastItemCoverUpdateSuccess": "כיסוי הפריט עודכן בהצלחה", + "ToastItemDetailsUpdateFailed": "עדכון פרטי הפריט נכשל", + "ToastItemDetailsUpdateSuccess": "פרטי הפריט עודכנו בהצלחה", + "ToastItemDetailsUpdateUnneeded": "לא נדרשים עדכונים לפרטי הפריט", + "ToastItemMarkedAsFinishedFailed": "סימון כפריט הושלם נכשל", + "ToastItemMarkedAsFinishedSuccess": "הפריט סומן כהושלם בהצלחה", + "ToastItemMarkedAsNotFinishedFailed": "סימון כפריט שלא הושלם נכשל", + "ToastItemMarkedAsNotFinishedSuccess": "הפריט סומן כלא הושלם בהצלחה", + "ToastLibraryCreateFailed": "יצירת הספרייה נכשלה", + "ToastLibraryCreateSuccess": "הספרייה \"{0}\" נוצרה בהצלחה", + "ToastLibraryDeleteFailed": "מחיקת הספרייה נכשלה", + "ToastLibraryDeleteSuccess": "הספרייה נמחקה בהצלחה", + "ToastLibraryScanFailedToStart": "הפעלת הסריקה נכשלה", + "ToastLibraryScanStarted": "הסריקה של הספרייה החלה", + "ToastLibraryUpdateFailed": "עדכון הספרייה נכשל", + "ToastLibraryUpdateSuccess": "הספרייה \"{0}\" עודכנה בהצלחה", + "ToastPlaylistCreateFailed": "יצירת רשימת השמעה נכשלה", + "ToastPlaylistCreateSuccess": "רשימת השמעה נוצרה בהצלחה", + "ToastPlaylistRemoveFailed": "הסרת רשימת השמעה נכשלה", + "ToastPlaylistRemoveSuccess": "רשימת השמעה הוסרה בהצלחה", + "ToastPlaylistUpdateFailed": "עדכון רשימת השמעה נכשל", + "ToastPlaylistUpdateSuccess": "רשימת השמעה עודכנה בהצלחה", + "ToastPodcastCreateFailed": "יצירת הפודקאסט נכשלה", + "ToastPodcastCreateSuccess": "הפודקאסט נוצר בהצלחה", + "ToastRemoveItemFromCollectionFailed": "הסרת הפריט מהאוסף נכשלה", + "ToastRemoveItemFromCollectionSuccess": "הפריט הוסר מהאוסף בהצלחה", + "ToastRSSFeedCloseFailed": "סגירת הערוץ RSS נכשלה", + "ToastRSSFeedCloseSuccess": "הערוץ RSS נסגר בהצלחה", + "ToastSendEbookToDeviceFailed": "שליחת הספר אל המכשיר נכשלה", + "ToastSendEbookToDeviceSuccess": "הספר נשלח אל המכשיר \"{0}\"", + "ToastSeriesUpdateFailed": "עדכון הסדרה נכשל", + "ToastSeriesUpdateSuccess": "הסדרה עודכנה בהצלחה", + "ToastSessionDeleteFailed": "מחיקת הפעולה נכשלה", + "ToastSessionDeleteSuccess": "הפעולה נמחקה בהצלחה", + "ToastSocketConnected": "חיבור קצה התקשורת בוצע בהצלחה", + "ToastSocketDisconnected": "התנתקות קצה התקשורת בוצעה", + "ToastSocketFailedToConnect": "התחברות קצה התקשורת נכשלה", + "ToastUserDeleteFailed": "מחיקת המשתמש נכשלה", + "ToastUserDeleteSuccess": "המשתמש נמחק בהצלחה" +} \ No newline at end of file From 752268effbbf44b7fd4735993462c7afa484265b Mon Sep 17 00:00:00 2001 From: dor <dor@raananos.com> Date: Sat, 16 Mar 2024 21:00:44 +0200 Subject: [PATCH 0452/2145] mid point proofing --- client/strings/he.json | 198 ++++++++++++++++++++--------------------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/client/strings/he.json b/client/strings/he.json index 582663c6..63295c46 100644 --- a/client/strings/he.json +++ b/client/strings/he.json @@ -11,12 +11,12 @@ "ButtonAuthors": "יוצרים", "ButtonBrowseForFolder": "עיין בתיקייה", "ButtonCancel": "בטל", - "ButtonCancelEncode": "בטל הצפנה", - "ButtonChangeRootPassword": "שנה סיסמת שורש", + "ButtonCancelEncode": "בטל קידוד", + "ButtonChangeRootPassword": "שנה סיסמת root", "ButtonCheckAndDownloadNewEpisodes": "בדוק והורד פרקים חדשים", "ButtonChooseAFolder": "בחר תיקייה", "ButtonChooseFiles": "בחר קבצים", - "ButtonClearFilter": "נקה פילטר", + "ButtonClearFilter": "נקה סינון", "ButtonCloseFeed": "סגור פיד", "ButtonCollections": "אוספים", "ButtonConfigureScanner": "הגדר סורק", @@ -31,18 +31,18 @@ "ButtonFullPath": "נתיב מלא", "ButtonHide": "הסתר", "ButtonHome": "בית", - "ButtonIssues": "בעיות", + "ButtonIssues": "תקלות", "ButtonJumpBackward": "דלג אחורה", "ButtonJumpForward": "דלג קדימה", - "ButtonLatest": "אחרון", + "ButtonLatest": "חדש ביותר", "ButtonLibrary": "ספרייה", "ButtonLogout": "התנתק", "ButtonLookup": "חפש", "ButtonManageTracks": "נהל רצועות", - "ButtonMapChapterTitles": "מפה של כותרות פרק", + "ButtonMapChapterTitles": "מפה כותרות פרקים", "ButtonMatchAllAuthors": "התאם את כל היוצרים", "ButtonMatchBooks": "התאם ספרים", - "ButtonNevermind": "אל תדאג", + "ButtonNevermind": "לא משנה", "ButtonNext": "הבא", "ButtonNextChapter": "פרק הבא", "ButtonOk": "אישור", @@ -59,15 +59,15 @@ "ButtonPurgeMediaProgress": "נקה את ההתקדמות במדיה", "ButtonQueueAddItem": "הוסף לתור", "ButtonQueueRemoveItem": "הסר מהתור", - "ButtonQuickMatch": "התאם מהר", + "ButtonQuickMatch": "התאמה מהירה", "ButtonRead": "קרא", "ButtonRefresh": "רענן", "ButtonRemove": "הסר", "ButtonRemoveAll": "הסר הכל", "ButtonRemoveAllLibraryItems": "הסר את כל פריטי הספרייה", - "ButtonRemoveFromContinueListening": "הסר מההמשך להאזנה", - "ButtonRemoveFromContinueReading": "הסר מההמשך לקריאה", - "ButtonRemoveSeriesFromContinueSeries": "הסר סדרה מהמשך לסדרות", + "ButtonRemoveFromContinueListening": "הסר מ- המשך האזנה", + "ButtonRemoveFromContinueReading": "הסר מ- המשך קריאה", + "ButtonRemoveSeriesFromContinueSeries": "הסר סדרה מ- המשך סדרה", "ButtonReScan": "סרוק מחדש", "ButtonReset": "איפוס", "ButtonResetToDefault": "איפוס לברירת המחדל", @@ -80,12 +80,12 @@ "ButtonSearch": "חפש", "ButtonSelectFolderPath": "בחר נתיב לתיקייה", "ButtonSeries": "סדרה", - "ButtonSetChaptersFromTracks": "קבע פרקים מרצועות", - "ButtonShare": "שתף", + "ButtonSetChaptersFromTracks": "קבע פרקים לפי הרצועות", + "ButtonShare": "שיתוף", "ButtonShiftTimes": "הזז זמנים", "ButtonShow": "הצג", - "ButtonStartM4BEncode": "התחל הצפנה M4B", - "ButtonStartMetadataEmbed": "התחל הטמעת מטאדאטה", + "ButtonStartM4BEncode": "התחל קידוד M4B", + "ButtonStartMetadataEmbed": "התחל הטמעת מטא-נתונים", "ButtonSubmit": "שלח", "ButtonTest": "בדיקה", "ButtonUpload": "העלה", @@ -96,14 +96,14 @@ "ButtonUserEdit": "ערוך משתמש {0}", "ButtonViewAll": "הצג הכול", "ButtonYes": "כן", - "ErrorUploadFetchMetadataAPI": "שגיאה בשליפת מטאדאטה", - "ErrorUploadFetchMetadataNoResults": "לא ניתן לשלוף מטאדאטה - נסה לעדכן כותרת ו/או יוצר", - "ErrorUploadLacksTitle": "חייב להיות כותרת", + "ErrorUploadFetchMetadataAPI": "שגיאה בשליפת מטא-נתונים", + "ErrorUploadFetchMetadataNoResults": "לא ניתן לשלוף מטא-נתונים - נסה לעדכן כותרת ו/או יוצר", + "ErrorUploadLacksTitle": "חובה לתת כותרת", "HeaderAccount": "חשבון", "HeaderAdvanced": "מתקדם", - "HeaderAppriseNotificationSettings": "הגדרות התראה ב-Apprise", - "HeaderAudiobookTools": "כלים לניהול קבצי אודיו", - "HeaderAudioTracks": "רצועות אודיו", + "HeaderAppriseNotificationSettings": "הגדרות התראות של Apprise", + "HeaderAudiobookTools": "כלים לניהול קבצי ספרים קוליים", + "HeaderAudioTracks": "רצועות קול", "HeaderAuthentication": "אימות", "HeaderBackups": "גיבויים", "HeaderChangePassword": "שנה סיסמה", @@ -113,60 +113,60 @@ "HeaderCollectionItems": "פריטי אוסף", "HeaderCover": "כריכה", "HeaderCurrentDownloads": "הורדות נוכחיות", - "HeaderCustomMetadataProviders": "ספקי מטאדאטה מותאמים", + "HeaderCustomMetadataProviders": "ספקי מטא-נתונים מותאמים אישית", "HeaderDetails": "פרטים", "HeaderDownloadQueue": "תור הורדה", "HeaderEbookFiles": "קבצי ספר אלקטרוני", "HeaderEmail": "אימייל", "HeaderEmailSettings": "הגדרות אימייל", "HeaderEpisodes": "פרקים", - "HeaderEreaderDevices": "התקני קריאה", - "HeaderEreaderSettings": "הגדרות קריאה", + "HeaderEreaderDevices": "התקני קריאה דיגיטליים", + "HeaderEreaderSettings": "הגדרות התקני קריאה דיגיטליים", "HeaderFiles": "קבצים", "HeaderFindChapters": "מצא פרקים", "HeaderIgnoredFiles": "קבצים שנתעלמו", "HeaderItemFiles": "קבצי פריט", - "HeaderItemMetadataUtils": "כלי מטאדאטה פריט", + "HeaderItemMetadataUtils": "כלי מטא-נתונים", "HeaderLastListeningSession": "הפעלת האזנה אחרונה", - "HeaderLatestEpisodes": "הפרקים האחרונים", + "HeaderLatestEpisodes": "הפרקים העדכניים ביותר", "HeaderLibraries": "ספריות", "HeaderLibraryFiles": "קבצי ספרייה", "HeaderLibraryStats": "סטטיסטיקות ספרייה", "HeaderListeningSessions": "הפעלות האזנה", "HeaderListeningStats": "סטטיסטיקות האזנה", - "HeaderLogin": "כניסה", + "HeaderLogin": "התחברות", "HeaderLogs": "לוגים", "HeaderManageGenres": "נהל ז'אנרים", "HeaderManageTags": "נהל תגיות", "HeaderMapDetails": "מפה פרטים", "HeaderMatch": "התאם", - "HeaderMetadataOrderOfPrecedence": "סדר העדפת מטאדאטה", - "HeaderMetadataToEmbed": "מטאדאטה להטמעה", + "HeaderMetadataOrderOfPrecedence": "סדר העדפת מטא-נתונים", + "HeaderMetadataToEmbed": "מטא-נתונים להטמעה", "HeaderNewAccount": "חשבון חדש", "HeaderNewLibrary": "ספרייה חדשה", "HeaderNotifications": "התראות", "HeaderOpenIDConnectAuthentication": "אימות OpenID Connect", - "HeaderOpenRSSFeed": "פתח פיד RSS", + "HeaderOpenRSSFeed": "פתח ערוץ RSS", "HeaderOtherFiles": "קבצים אחרים", "HeaderPasswordAuthentication": "אימות סיסמה", "HeaderPermissions": "הרשאות", "HeaderPlayerQueue": "תור ניגון", "HeaderPlaylist": "רשימת השמעה", "HeaderPlaylistItems": "פריטי רשימת השמעה", - "HeaderPodcastsToAdd": "פודקאסטים להוסיף", + "HeaderPodcastsToAdd": "פודקאסטים להוספה", "HeaderPreviewCover": "תצוגה מקדימה של כריכה", "HeaderRemoveEpisode": "הסר פרק", "HeaderRemoveEpisodes": "הסר {0} פרקים", - "HeaderRSSFeedGeneral": "פיד RSS כללי", - "HeaderRSSFeedIsOpen": "הפיד RSS פתוח", - "HeaderRSSFeeds": "פידי RSS", + "HeaderRSSFeedGeneral": "פרטי ערוץ RSS", + "HeaderRSSFeedIsOpen": "ערוץ RSS פתוח", + "HeaderRSSFeeds": "ערוצי RSS", "HeaderSavedMediaProgress": "התקדמות מדיה שמורה", - "HeaderSchedule": "מתזמן", + "HeaderSchedule": "תיזמון", "HeaderScheduleLibraryScans": "קבע סריקות ספרייה אוטומטיות", "HeaderSession": "הפעלה", - "HeaderSetBackupSchedule": "קבע מתז גיבוי", + "HeaderSetBackupSchedule": "קבע לוח זמנים לגיבוי", "HeaderSettings": "הגדרות", - "HeaderSettingsDisplay": "הצגה", + "HeaderSettingsDisplay": "תצוגה", "HeaderSettingsExperimental": "תכונות ניסיוניות", "HeaderSettingsGeneral": "כללי", "HeaderSettingsScanner": "סורק", @@ -175,7 +175,7 @@ "HeaderStatsLongestItems": "הפריטים הארוכים ביותר (בשעות)", "HeaderStatsMinutesListeningChart": "דקות האזנה (בימים האחרונים)", "HeaderStatsRecentSessions": "הפעלות אחרונות", - "HeaderStatsTop10Authors": "היוצרים המובילים 10", + "HeaderStatsTop10Authors": "10 היוצרים המובילים", "HeaderStatsTop5Genres": "הז'אנרים המובילים 5", "HeaderTableOfContents": "תוכן העניינים", "HeaderTools": "כלים", @@ -201,19 +201,19 @@ "LabelAdminUsersOnly": "רק מנהלים", "LabelAll": "הכל", "LabelAllUsers": "כל המשתמשים", - "LabelAllUsersExcludingGuests": "כל המשתמשים ללא אורחים", + "LabelAllUsersExcludingGuests": "כל המשתמשים, ללא אורחים", "LabelAllUsersIncludingGuests": "כל המשתמשים כולל אורחים", - "LabelAlreadyInYourLibrary": "כבר בספרייה שלך", + "LabelAlreadyInYourLibrary": "כבר קיים בספרייה שלך", "LabelAppend": "הוסף לסוף", "LabelAuthor": "יוצר", "LabelAuthorFirstLast": "יוצר (שם פרטי שם משפחה)", "LabelAuthorLastFirst": "יוצר (שם משפחה, שם פרטי)", "LabelAuthors": "יוצרים", "LabelAutoDownloadEpisodes": "הורד פרקים באופן אוטומטי", - "LabelAutoFetchMetadata": "שלף מטאדאטה באופן אוטומטי", - "LabelAutoFetchMetadataHelp": "משיג מטאדאטה עבור כותרת, יוצר וסדרה כדי לשפר את תהליך ההעלאה. ייתכן שיהיה על יכולתך להתאים מטאדאטה נוספת לאחר ההעלאה.", + "LabelAutoFetchMetadata": "חפש והורד מטא-נתונים באופן אוטומטי", + "LabelAutoFetchMetadataHelp": "מחפש ומוריד מטא-נתונים לשדות כותרת, יוצר וסדרה כדי לשפר את תהליך ההעלאה. ייתכן שיהיה צורך להתאים מטא-נתונים נוסף לאחר ההעלאה.", "LabelAutoLaunch": "הפעלה אוטומטית", - "LabelAutoLaunchDescription": "הפניה אוטומטית לספק האימות כאשר מגיעים לדף ההתחברות (ניתן להפעיל ידנית במסלול <code>/login?autoLaunch=0</code>)", + "LabelAutoLaunchDescription": "הפניה אוטומטית לספק האימות כאשר מגיעים לדף ההתחברות (ניתן להפעיל ידנית בכתובת <code>/login?autoLaunch=0</code>)", "LabelAutoRegister": "הרשמה אוטומטית", "LabelAutoRegisterDescription": "יצירת משתמשים חדשים אוטומטית לאחר התחברות", "LabelBackToUser": "חזרה למשתמש", @@ -223,18 +223,18 @@ "LabelBackupsMaxBackupSize": "גודל הגיבוי המרבי (בג'יגה-בייט)", "LabelBackupsMaxBackupSizeHelp": "כהגנה על עצמך מפני תצורה שגויה, הגיבויים ייכשלו אם הם יעברו את הגודל שהוגדר.", "LabelBackupsNumberToKeep": "מספר הגיבויים לשמירה", - "LabelBackupsNumberToKeepHelp": "יש להסיר רק גיבוי אחד בכל פעם, לכן אם יש לך כבר יותר מגיבוי אחד יש להסיר אותם באופן ידני.", - "LabelBitrate": "ביטרייט", + "LabelBackupsNumberToKeepHelp": "רק גיבוי אחד יוסר בכל פעם, לכן אם יש לך כבר יותר מגיבוי אחד יש להסיר אותם באופן ידני.", + "LabelBitrate": "קצב סיביות", "LabelBooks": "ספרים", "LabelButtonText": "טקסט לחצן", "LabelChangePassword": "שינוי סיסמה", "LabelChannels": "ערוצים", "LabelChapters": "פרקים", - "LabelChaptersFound": "פרקים נמצאו", + "LabelChaptersFound": "פרקים שנמצאו", "LabelChapterTitle": "כותרת הפרק", "LabelClickForMoreInfo": "לחץ למידע נוסף", "LabelClosePlayer": "סגור נגן", - "LabelCodec": "קודק", + "LabelCodec": "Codec", "LabelCollapseSeries": "צמצום סדרה", "LabelCollection": "אוסף", "LabelCollections": "אוספים", @@ -243,15 +243,15 @@ "LabelContinueListening": "המשך האזנה", "LabelContinueReading": "המשך קריאה", "LabelContinueSeries": "המשך סדרה", - "LabelCover": "כיסוי", - "LabelCoverImageURL": "כתובת התמונה המצויה ברשת", + "LabelCover": "כריכה", + "LabelCoverImageURL": "כתובת התמונה ברשת", "LabelCreatedAt": "נוצר בתאריך", - "LabelCronExpression": "ביטוי קרון", + "LabelCronExpression": "Cron Expression", "LabelCurrent": "נוכחי", "LabelCurrently": "כעת:", - "LabelCustomCronExpression": "ביטוי קרון מותאם אישית:", - "LabelDatetime": "תאריך ושעה", - "LabelDeleteFromFileSystemCheckbox": "מחיקה מהמערכת הקבצים (הסר סימון למחיקה רק מהמסד נתונים)", + "LabelCustomCronExpression": "Custom Cron Expression:", + "LabelDatetime": "Datetime", + "LabelDeleteFromFileSystemCheckbox": "מחיקה מהמערכת הקבצים (הסר סימון למחיקה רק ממסד הנתונים)", "LabelDescription": "תיאור", "LabelDeselectAll": "הסר בחירת כל הפריטים", "LabelDevice": "התקן", @@ -269,27 +269,27 @@ "LabelEbooks": "ספרים אלקטרוניים", "LabelEdit": "עריכה", "LabelEmail": "דואר אלקטרוני", - "LabelEmailSettingsFromAddress": "כתובת מאיתה", + "LabelEmailSettingsFromAddress": "מאת", "LabelEmailSettingsSecure": "מאובטח", - "LabelEmailSettingsSecureHelp": "אם נכון, החיבור ישתמש ב-TLS בעת התחברות לשרת. אם לא, תישתמש חיבור זה ב-TLS אם השרת תומך בהרחבת STARTTLS. ברוב המקרים עדיף להגדיר ערך זה כנכון אם אתה מחבר לפורט 465. לפורט 587 או 25 שמור על ערך זה כשקר.", - "LabelEmailSettingsTestAddress": "כתובת לבדיקת מבנה", - "LabelEmbeddedCover": "כיסוי משובץ", + "LabelEmailSettingsSecureHelp": "אם מופעל, החיבור ישתמש ב-TLS בעת ההתחברות לשרת. אם לא, אז TLS יהיה בשימוש אם השרת תומך בהרחבת STARTTLS. ברוב המקרים מומלץ להפעיל את הגדרה זו אם אתה מתחבר לפורט 465. לפורט 587 או 25, השאר כבוי. (from nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "כתובת לבדיקה", + "LabelEmbeddedCover": "כריכה מוטמעת", "LabelEnable": "הפעל", "LabelEnd": "סיום", "LabelEpisode": "פרק", "LabelEpisodeTitle": "כותרת הפרק", "LabelEpisodeType": "סוג הפרק", "LabelExample": "דוגמה", - "LabelExplicit": "ברור", + "LabelExplicit": "בוטה", "LabelFeedURL": "כתובת ערוץ", - "LabelFetchingMetadata": "מושכים מטא-נתונים", + "LabelFetchingMetadata": "מושך מטא-נתונים", "LabelFile": "קובץ", - "LabelFileBirthtime": "זמן הולדת הקובץ", - "LabelFileModified": "הקובץ הוחלף", + "LabelFileBirthtime": "זמן יצירת הקובץ", + "LabelFileModified": "הקובץ שונה", "LabelFilename": "שם הקובץ", "LabelFilterByUser": "סינון לפי משתמש", "LabelFindEpisodes": "מצא פרקים", - "LabelFinished": "סיים", + "LabelFinished": "הושלם", "LabelFolder": "תיקייה", "LabelFolders": "תיקיות", "LabelFontBold": "מודגש", @@ -300,10 +300,10 @@ "LabelFormat": "תבנית", "LabelGenre": "ז'אנר", "LabelGenres": "ז'אנרים", - "LabelHardDeleteFile": "מחיקה קשה של הקובץ", - "LabelHasEbook": "יש ספר אלקטרוני", - "LabelHasSupplementaryEbook": "יש ספר אלקטרוני תוספתי", - "LabelHighestPriority": "עדיפות הגבוהה ביותר", + "LabelHardDeleteFile": "מחיקה חזקה של הקובץ", + "LabelHasEbook": "ספר אלקטרוני קיים", + "LabelHasSupplementaryEbook": "קיים ספר אלקטרוני נלווה", + "LabelHighestPriority": "העדיפות הגבוהה ביותר", "LabelHost": "מארח", "LabelHour": "שעה", "LabelIcon": "סמל", @@ -315,7 +315,7 @@ "LabelIntervalCustomDailyWeekly": "מותאם אישית יומי/שבועי", "LabelIntervalEvery12Hours": "כל 12 שעות", "LabelIntervalEvery15Minutes": "כל 15 דקות", - "LabelIntervalEvery2Hours": "כל 2 שעות", + "LabelIntervalEvery2Hours": "כל שעתיים", "LabelIntervalEvery30Minutes": "כל 30 דקות", "LabelIntervalEvery6Hours": "כל 6 שעות", "LabelIntervalEveryDay": "כל יום", @@ -324,11 +324,11 @@ "LabelInvert": "הפוך", "LabelItem": "פריט", "LabelLanguage": "שפה", - "LabelLanguageDefaultServer": "שפת השרת ברירת המחדל", - "LabelLastBookAdded": "הספר האחרון שהוסף", + "LabelLanguageDefaultServer": "שפת ברירת המחדל של השרת", + "LabelLastBookAdded": "הספר האחרון שנוסף", "LabelLastBookUpdated": "הספר האחרון שעודכן", "LabelLastSeen": "נראה לאחרונה", - "LabelLastTime": "הפעם האחרונה", + "LabelLastTime": "הזמן האחרון", "LabelLastUpdate": "עדכון אחרון", "LabelLayout": "פריסה", "LabelLayoutSinglePage": "דף בודד", @@ -339,15 +339,15 @@ "LabelLibraryItem": "פריט ספרייה", "LabelLibraryName": "שם הספרייה", "LabelLimit": "מגבלה", - "LabelLineSpacing": "רווח שורות", + "LabelLineSpacing": "ריווח שורות", "LabelListenAgain": "האזן שוב", "LabelLogLevelDebug": "דיבוג", "LabelLogLevelInfo": "מידע", "LabelLogLevelWarn": "אזהרה", "LabelLookForNewEpisodesAfterDate": "חפש פרקים חדשים לאחר תאריך זה", - "LabelLowestPriority": "עדיפות הנמוכה ביותר", + "LabelLowestPriority": "העדיפות הנמוכה ביותר", "LabelMatchExistingUsersBy": "התאם משתמשים קיימים לפי", - "LabelMatchExistingUsersByDescription": "משמש לחיבור משתמשים קיימים. לאחר החיבור, המשתמשים יתאמו לפי זיהוי ייחודי מספק ה-SSO שלך", + "LabelMatchExistingUsersByDescription": "משמש לחיבור משתמשים קיימים. לאחר החיבור, המשתמשים יותאמו לפי זיהוי ייחודי מספק ה-SSO שלך", "LabelMediaPlayer": "נגן מדיה", "LabelMediaType": "סוג מדיה", "LabelMetadataOrderOfPrecedenceDescription": "מקורות המטא-נתונים עם עדיפות גבוהה יחליפו מקורות עם עדיפות נמוכה יותר", @@ -364,25 +364,25 @@ "LabelMore": "עוד", "LabelMoreInfo": "מידע נוסף", "LabelName": "שם", - "LabelNarrator": "נרטור", - "LabelNarrators": "נרטורים", + "LabelNarrator": "מספר", + "LabelNarrators": "מספרים", "LabelNew": "חדש", - "LabelNewestAuthors": "סופרים החדשים ביותר", + "LabelNewestAuthors": "הסופרים החדשים ביותר", "LabelNewestEpisodes": "הפרקים החדשים ביותר", "LabelNewPassword": "סיסמה חדשה", - "LabelNextBackupDate": "תאריך גיבוי הבא", + "LabelNextBackupDate": "תאריך הגיבוי הבא", "LabelNextScheduledRun": "הרצה מתוזמנת הבאה", "LabelNoEpisodesSelected": "לא נבחרו פרקים", "LabelNotes": "הערות", "LabelNotFinished": "לא הושלם", - "LabelNotificationAppriseURL": "כתובת URL של התראה", + "LabelNotificationAppriseURL": "כתובות Apprise", "LabelNotificationAvailableVariables": "משתנים זמינים", "LabelNotificationBodyTemplate": "תבנית גוף", "LabelNotificationEvent": "אירוע התראה", "LabelNotificationsMaxFailedAttempts": "מספר הניסיונות הנכשלים המרבי", - "LabelNotificationsMaxFailedAttemptsHelp": "ההתראות מושבתות לאחר שהן נכשלות לשלוח מספר זה פעמים", + "LabelNotificationsMaxFailedAttemptsHelp": "ההתראות מושבתות לאחר שהן נכשלות לשלוח מספר פעמים זה", "LabelNotificationsMaxQueueSize": "גודל התור המרבי לאירועי התראה", - "LabelNotificationsMaxQueueSizeHelp": "האירועים מוגבלים לשליחה אחת לשנייה. האירועים יתעלמו אם התור מלא בגודלו המרבי. זה מונע ספאם בהתראות.", + "LabelNotificationsMaxQueueSizeHelp": "האירועים מוגבלים לשליחה אחת לשנייה. האירועים יתעלמו אם התור מלא. הגדרה זו נועדה למנוע ספאם התראות.", "LabelNotificationTitleTemplate": "תבנית כותרת", "LabelNotStarted": "לא התחיל", "LabelNumberOfBooks": "מספר הספרים", @@ -393,11 +393,11 @@ "LabelPath": "נתיב", "LabelPermissionsAccessAllLibraries": "ניתן לגשת לכל הספריות", "LabelPermissionsAccessAllTags": "ניתן לגשת לכל התגיות", - "LabelPermissionsAccessExplicitContent": "ניתן לגשת לתוכן מפורט", - "LabelPermissionsDelete": "ניתן למחוק", - "LabelPermissionsDownload": "ניתן להוריד", - "LabelPermissionsUpdate": "ניתן לעדכן", - "LabelPermissionsUpload": "ניתן להעלות", + "LabelPermissionsAccessExplicitContent": "ניתן לגשת לתוכן בוטה", + "LabelPermissionsDelete": "מותר למחוק", + "LabelPermissionsDownload": "מותר להוריד", + "LabelPermissionsUpdate": "מותר לעדכן", + "LabelPermissionsUpload": "מותר להעלות", "LabelPersonalYearReview": "השנה שלך בסקירה ({0})", "LabelPhotoPathURL": "נתיב/URL לתמונה", "LabelPlaylists": "רשימות השמעה", @@ -406,9 +406,9 @@ "LabelPodcasts": "פודקאסטים", "LabelPodcastSearchRegion": "אזור חיפוש פודקאסט", "LabelPodcastType": "סוג פודקאסט", - "LabelPort": "יציאה", - "LabelPrefixesToIgnore": "קידומות להתעלמות (קסה אינסנסיטיבית)", - "LabelPreventIndexing": "מנע את האינדוקסציה של הפיד שלך על ידי ספריות אייטונס וגוגל פודקאסט", + "LabelPort": "פורט", + "LabelPrefixesToIgnore": "קידומות להתעלמות (מתעלם מאותיות גדולות/קטנות)", + "LabelPreventIndexing": "מנע רישום של הערוץ שלך על ידי ספריות אייטונס וגוגל פודקאסט", "LabelPrimaryEbook": "ספר אלקטרוני ראשי", "LabelProgress": "התקדמות", "LabelProvider": "ספק", @@ -417,21 +417,21 @@ "LabelPublishYear": "שנת הפרסום", "LabelRead": "קריאה", "LabelReadAgain": "קרא שוב", - "LabelReadEbookWithoutProgress": "קרוא ספר אלקטרוני בלי לשמור התקדמות", + "LabelReadEbookWithoutProgress": "קרא/י ספר אלקטרוני ללא שמירת התקדמות", "LabelRecentlyAdded": "נוסף לאחרונה", "LabelRecentSeries": "סדרות אחרונות", "LabelRecommended": "מומלץ", "LabelRedo": "עשה שוב", "LabelRegion": "אזור", - "LabelReleaseDate": "תאריך שחרור", - "LabelRemoveCover": "הסר כיסוי", + "LabelReleaseDate": "תאריך הוצאה לאור", + "LabelRemoveCover": "הסר כריכה", "LabelRowsPerPage": "שורות לעמוד", "LabelRSSFeedCustomOwnerEmail": "אימייל בעלים מותאם אישית", "LabelRSSFeedCustomOwnerName": "שם בעלים מותאם אישית", "LabelRSSFeedOpen": "פתח ערוץ RSS", - "LabelRSSFeedPreventIndexing": "מנע אינדוקסציה", - "LabelRSSFeedSlug": "שם תמצאות RSS", - "LabelRSSFeedURL": "URL של פיד RSS", + "LabelRSSFeedPreventIndexing": "מנע רישום", + "LabelRSSFeedSlug": "Slug של ערוץ ה-RSS", + "LabelRSSFeedURL": "כתובת ערוץ ה-RSS", "LabelSearchTerm": "מונח חיפוש", "LabelSearchTitle": "כותרת חיפוש", "LabelSearchTitleOrASIN": "כותרת חיפוש או ASIN", @@ -449,11 +449,11 @@ "LabelSetEbookAsSupplementary": "קבע כספר אלקטרוני נלווה", "LabelSettingsAudiobooksOnly": "רק ספרי קול", "LabelSettingsAudiobooksOnlyHelp": "הפעלת ההגדרה הזו תתעלם מקבצי ספרים אלקטרוניים אלא אם כן הם נמצאים בתיקיית ספרי קול, שבמקרה זה יקבעו כספרים אלקטרוניים נלווים", - "LabelSettingsBookshelfViewHelp": "עיצוב סקיומורפי עם מדפים עץ", - "LabelSettingsChromecastSupport": "תמיכת Chromecast", + "LabelSettingsBookshelfViewHelp": "עיצוב סקאומורפי עם מדפי עץ", + "LabelSettingsChromecastSupport": "תמיכה ב-Chromecast", "LabelSettingsDateFormat": "פורמט תאריך", - "LabelSettingsDisableWatcher": "השבת צופה", - "LabelSettingsDisableWatcherForLibrary": "השבת צופה תיקייה עבור ספרייה", + "LabelSettingsDisableWatcher": "השבת עוקב", + "LabelSettingsDisableWatcherForLibrary": "השבת עוקב תיקייה עבור ספרייה", "LabelSettingsDisableWatcherHelp": "מבטל את הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", "LabelSettingsEnableWatcher": "הפעל צופה", "LabelSettingsEnableWatcherForLibrary": "הפעל צופה תיקייה עבור ספרייה", @@ -679,7 +679,7 @@ "MessageReportBugsAndContribute": "דווח על באגים, בקש תכונות חדשות, ותרום ב-", "MessageResetChaptersConfirm": "האם אתה בטוח שברצונך לאפס את הפרקים ולבטל את השינויים שביצעת?", "MessageRestoreBackupConfirm": "האם אתה בטוח שברצונך לשחזר את הגיבוי שנוצר ב", - "MessageRestoreBackupWarning": "שחזור גיבוי ימחק את כל מסד הנתונים השוכן ב /config ואת תמונות הכריכה ב- /metadata/items & /metadata/authors.<br /><br />גיבויים אינם משנים קבצים בתיקיות הספרייה שלך. אם הגדרות השרת מופעלות כדי לאחסן תמונות כריכה ומטאדאטה בתיקיות הספרייה שלך אז אלה לא יגובו או ימחקו.<br /><br />כל הלקוחות המשתמשים בשרת שלך יתעדכנו באופן אוטומטי.", + "MessageRestoreBackupWarning": "שחזור גיבוי ימחק את כל מסד הנתונים השוכן ב /config ואת תמונות הכריכה ב- /metadata/items & /metadata/authors.<br /><br />גיבויים אינם משנים קבצים בתיקיות הספרייה שלך. אם הגדרות השרת מופעלות כדי לאחסן תמונות כריכה ומטא-נתונים בתיקיות הספרייה שלך אז אלה לא יגובו או ימחקו.<br /><br />כל הלקוחות המשתמשים בשרת שלך יתעדכנו באופן אוטומטי.", "MessageSearchResultsFor": "תוצאות חיפוש עבור", "MessageSelected": "{0} נבחרו", "MessageServerCouldNotBeReached": "לא ניתן להגיע אל השרת", From a2b2a2d060e9c5392df21dbb17a7ac7b6eb2ad8c Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 16 Mar 2024 15:12:33 -0500 Subject: [PATCH 0453/2145] Fix:Applying backup not properly overwriting existing sqlite file - Fixed resetting api cache on backup - Added loading indicator in backups table - Fixed apply backup api not responding with 200 http status code - Added additional logging and failsafes --- client/components/tables/BackupsTable.vue | 14 ++++-- server/Database.js | 1 - server/controllers/BackupController.js | 7 ++- server/managers/ApiCacheManager.js | 10 +++++ server/managers/BackupManager.js | 54 ++++++++++++++++++++++- 5 files changed, 78 insertions(+), 8 deletions(-) diff --git a/client/components/tables/BackupsTable.vue b/client/components/tables/BackupsTable.vue index 08692a4d..bd3d074a 100644 --- a/client/components/tables/BackupsTable.vue +++ b/client/components/tables/BackupsTable.vue @@ -1,5 +1,5 @@ <template> - <div class="text-center mt-4"> + <div class="text-center mt-4 relative"> <div class="flex py-4"> <ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">{{ $strings.ButtonUploadBackup }}</ui-file-input> <div class="flex-grow" /> @@ -54,6 +54,10 @@ </div> </div> </prompt-dialog> + + <div v-if="isApplyingBackup" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/20 rounded-md"> + <ui-loading-indicator /> + </div> </div> </template> @@ -64,6 +68,7 @@ export default { showConfirmApply: false, selectedBackup: null, isBackingUp: false, + isApplyingBackup: false, processing: false, backups: [] } @@ -85,19 +90,21 @@ export default { }, confirm() { this.showConfirmApply = false + this.isApplyingBackup = true this.$axios .$get(`/api/backups/${this.selectedBackup.id}/apply`) .then(() => { - this.isBackingUp = false location.replace('/config/backups?backup=1') }) .catch((error) => { - this.isBackingUp = false console.error('Failed to apply backup', error) const errorMsg = error.response.data || this.$strings.ToastBackupRestoreFailed this.$toast.error(errorMsg) }) + .finally(() => { + this.isApplyingBackup = false + }) }, deleteBackupClick(backup) { if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) { @@ -180,7 +187,6 @@ export default { this.loadBackups() if (this.$route.query.backup) { this.$toast.success('Backup applied successfully') - this.$router.replace('/config') } } } diff --git a/server/Database.js b/server/Database.js index e3fabe1f..64dc518e 100644 --- a/server/Database.js +++ b/server/Database.js @@ -217,7 +217,6 @@ class Database { async disconnect() { Logger.info(`[Database] Disconnecting sqlite db`) await this.sequelize.close() - this.sequelize = null } /** diff --git a/server/controllers/BackupController.js b/server/controllers/BackupController.js index 8104623e..bd6caa0b 100644 --- a/server/controllers/BackupController.js +++ b/server/controllers/BackupController.js @@ -49,8 +49,13 @@ class BackupController { res.sendFile(req.backup.fullPath) } + /** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ apply(req, res) { - this.backupManager.requestApplyBackup(req.backup, res) + this.backupManager.requestApplyBackup(this.apiCacheManager, req.backup, res) } middleware(req, res, next) { diff --git a/server/managers/ApiCacheManager.js b/server/managers/ApiCacheManager.js index 1af069f3..bb99b8cb 100644 --- a/server/managers/ApiCacheManager.js +++ b/server/managers/ApiCacheManager.js @@ -22,6 +22,16 @@ class ApiCacheManager { this.cache.clear() } + /** + * Reset hooks and clear cache. Used when applying backups + */ + reset() { + Logger.info(`[ApiCacheManager] Resetting cache`) + + this.init() + this.cache.clear() + } + get middleware() { return (req, res, next) => { const key = { user: req.user.username, url: req.url } diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js index ef8ed643..40a74f14 100644 --- a/server/managers/BackupManager.js +++ b/server/managers/BackupManager.js @@ -146,23 +146,73 @@ class BackupManager { } } - async requestApplyBackup(backup, res) { + /** + * + * @param {import('./ApiCacheManager')} apiCacheManager + * @param {Backup} backup + * @param {import('express').Response} res + */ + async requestApplyBackup(apiCacheManager, backup, res) { + Logger.info(`[BackupManager] Applying backup at "${backup.fullPath}"`) + const zip = new StreamZip.async({ file: backup.fullPath }) const entries = await zip.entries() + + // Ensure backup has an absdatabase.sqlite file if (!Object.keys(entries).includes('absdatabase.sqlite')) { Logger.error(`[BackupManager] Cannot apply old backup ${backup.fullPath}`) + await zip.close() return res.status(500).send('Invalid backup file. Does not include absdatabase.sqlite. This might be from an older Audiobookshelf server.') } await Database.disconnect() - await zip.extract('absdatabase.sqlite', global.ConfigPath) + const dbPath = Path.join(global.ConfigPath, 'absdatabase.sqlite') + const tempDbPath = Path.join(global.ConfigPath, 'absdatabase-temp.sqlite') + + // Extract backup sqlite file to temporary path + await zip.extract('absdatabase.sqlite', tempDbPath) + Logger.info(`[BackupManager] Extracted backup sqlite db to temp path ${tempDbPath}`) + + // Verify extract - Abandon backup if sqlite file did not extract + if (!await fs.pathExists(tempDbPath)) { + Logger.error(`[BackupManager] Sqlite file not found after extract - abandon backup apply and reconnect db`) + await zip.close() + await Database.reconnect() + return res.status(500).send('Failed to extract sqlite db from backup') + } + + // Attempt to remove existing db file + try { + await fs.remove(dbPath) + } catch (error) { + // Abandon backup and remove extracted sqlite file if unable to remove existing db file + Logger.error(`[BackupManager] Unable to overwrite existing db file - abandon backup apply and reconnect db`, error) + await fs.remove(tempDbPath) + await zip.close() + await Database.reconnect() + return res.status(500).send(`Failed to overwrite sqlite db: ${error?.message || 'Unknown Error'}`) + } + + // Rename temp db + await fs.move(tempDbPath, dbPath) + Logger.info(`[BackupManager] Saved backup sqlite file at "${dbPath}"`) + + // Extract /metadata/items and /metadata/authors folders await zip.extract('metadata-items/', this.ItemsMetadataPath) await zip.extract('metadata-authors/', this.AuthorsMetadataPath) + await zip.close() + // Reconnect db await Database.reconnect() + // Reset api cache, set hooks again + await apiCacheManager.reset() + + res.sendStatus(200) + + // Triggers browser refresh for all clients SocketAuthority.emitter('backup_applied') } From 9713e94aed17ca9c8b22376ec7c3e9969826962e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 16 Mar 2024 15:41:35 -0500 Subject: [PATCH 0454/2145] Reformat login page with logo in top left --- client/pages/login.vue | 51 ++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 897d5604..3737bbda 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -1,5 +1,12 @@ <template> <div id="page-wrapper" class="w-full h-screen"> + <div class="absolute inset-0 px-6 py-3"> + <div class="flex items-center"> + <img src="~static/icon.svg" alt="Audiobookshelf Logo" class="w-10 min-w-10 h-10" /> + <h1 class="text-xl ml-4 hidden lg:block hover:underline">audiobookshelf</h1> + </div> + </div> + <div class="w-full flex h-full items-center justify-center"> <div v-if="criticalError" class="w-full max-w-md rounded border border-error border-opacity-25 bg-error bg-opacity-10 p-4"> <p class="text-center text-lg font-semibold">{{ $strings.MessageServerCouldNotBeReached }}</p> @@ -23,34 +30,34 @@ </div> </form> </div> - <div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40"> - <div class="flex justify-center mb-4"><img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-32 min-w-32 h-32" /></div> - <p class="text-3xl text-white text-center mb-4">{{ $strings.HeaderLogin }}</p> - + <div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 lg:-mt-40"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> + <p class="text-2xl font-semibold text-center text-white mb-4">{{ $strings.HeaderLogin }}</p> - <p v-if="loginCustomMessage" class="py-2 default-style mb-2" v-html="loginCustomMessage"></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> + <p v-if="loginCustomMessage" class="py-2 default-style mb-2" v-html="loginCustomMessage"></p> - <form v-show="login_local" @submit.prevent="submitForm"> - <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label> - <ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" /> + <p v-if="error" class="text-error text-center py-2">{{ error }}</p> - <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label> - <ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" inputName="password" /> - <div class="w-full flex justify-end py-3"> - <ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn> + <form v-show="login_local" @submit.prevent="submitForm"> + <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label> + <ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" /> + + <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label> + <ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" inputName="password" /> + <div class="w-full flex justify-end py-3"> + <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> - </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> From b9ffce166e9a853d3fa97bf22478a81512cba7a9 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 16 Mar 2024 15:55:13 -0500 Subject: [PATCH 0455/2145] Login page add overflow scroll for mobile landscape, update z index for logo --- client/pages/login.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index 3737bbda..94741648 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -1,13 +1,13 @@ <template> - <div id="page-wrapper" class="w-full h-screen"> - <div class="absolute inset-0 px-6 py-3"> + <div id="page-wrapper" class="w-full h-screen overflow-y-auto"> + <div class="absolute z-0 top-0 left-0 px-6 py-3"> <div class="flex items-center"> <img src="~static/icon.svg" alt="Audiobookshelf Logo" class="w-10 min-w-10 h-10" /> <h1 class="text-xl ml-4 hidden lg:block hover:underline">audiobookshelf</h1> </div> </div> - <div class="w-full flex h-full items-center justify-center"> + <div class="relative z-10 w-full flex h-full items-center justify-center"> <div v-if="criticalError" class="w-full max-w-md rounded border border-error border-opacity-25 bg-error bg-opacity-10 p-4"> <p class="text-center text-lg font-semibold">{{ $strings.MessageServerCouldNotBeReached }}</p> </div> @@ -31,7 +31,7 @@ </form> </div> <div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 lg:-mt-40"> - <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> + <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4"> <p class="text-2xl font-semibold text-center text-white mb-4">{{ $strings.HeaderLogin }}</p> <div class="w-full h-px bg-white bg-opacity-10 my-4" /> From d5c854d606967a41566cccc8da6ee70a539a0ac2 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 16 Mar 2024 16:35:05 -0500 Subject: [PATCH 0456/2145] Update:Add robots.txt and noindex meta tag --- client/nuxt.config.js | 3 ++- client/static/robots.txt | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 client/static/robots.txt diff --git a/client/nuxt.config.js b/client/nuxt.config.js index 6a347c23..b5659086 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -25,7 +25,8 @@ module.exports = { meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, - { hid: 'description', name: 'description', content: '' } + { hid: 'description', name: 'description', content: '' }, + { hid: 'robots', name: 'robots', content: 'noindex' } ], script: [], link: [ diff --git a/client/static/robots.txt b/client/static/robots.txt new file mode 100644 index 00000000..95458b58 --- /dev/null +++ b/client/static/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: / \ No newline at end of file From 166454ef43e3cdb42c644cdccdacddd3a880cd89 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 16 Mar 2024 17:15:33 -0500 Subject: [PATCH 0457/2145] Version bump v2.8.1 --- client/package-lock.json | 6 +++--- client/package.json | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 9d513691..a3435d74 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.8.0", + "version": "2.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.8.0", + "version": "2.8.1", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", @@ -16976,4 +16976,4 @@ } } } -} +} \ No newline at end of file diff --git a/client/package.json b/client/package.json index 55acee1b..5ce9766d 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.8.0", + "version": "2.8.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", @@ -36,4 +36,4 @@ "postcss": "^8.3.6", "tailwindcss": "^3.4.1" } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a62598af..a16b0c19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.8.0", + "version": "2.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.8.0", + "version": "2.8.1", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 88b1581f..0bcb88be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.8.0", + "version": "2.8.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From d562f6a69f4dc7c0538065b34530f757e7f3acc9 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sun, 17 Mar 2024 07:36:13 +0200 Subject: [PATCH 0458/2145] Change unit-tests.yml workflow to include conditional checkout step --- .github/workflows/unit-tests.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 24f6398c..80b9855d 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -11,13 +11,19 @@ on: jobs: run-unit-tests: + name: Run Unit Tests runs-on: ubuntu-latest steps: - - name: Checkout code + - name: Checkout (push/pull request) + uses: actions/checkout@v4 + if: github.event_name != 'workflow_dispatch' + + - name: Checkout (workflow_dispatch) uses: actions/checkout@v4 with: - ref: ${{ github.event_name != 'workflow_dispatch' && github.ref_name || inputs.ref}} + ref: inputs.ref + if: github.event_name == 'workflow_dispatch' - name: Set up Node.js uses: actions/setup-node@v4 From f938fca2c701ee26e0843a3a8a9eeb5158109828 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sun, 17 Mar 2024 07:57:28 +0200 Subject: [PATCH 0459/2145] Fix bug in workflow_dispatch checkout step --- .github/workflows/unit-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 80b9855d..695696c6 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout (workflow_dispatch) uses: actions/checkout@v4 with: - ref: inputs.ref + ref: ${{ inputs.ref }} if: github.event_name == 'workflow_dispatch' - name: Set up Node.js From be4eb28b210f430bbe9f12221288ba927d987c86 Mon Sep 17 00:00:00 2001 From: dor <dor@raananos.com> Date: Sun, 17 Mar 2024 22:25:49 +0200 Subject: [PATCH 0460/2145] finished proofing Hebrew translation --- client/strings/he.json | 190 ++++++++++++++++++++--------------------- 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/client/strings/he.json b/client/strings/he.json index 63295c46..435f60ad 100644 --- a/client/strings/he.json +++ b/client/strings/he.json @@ -455,32 +455,32 @@ "LabelSettingsDisableWatcher": "השבת עוקב", "LabelSettingsDisableWatcherForLibrary": "השבת עוקב תיקייה עבור ספרייה", "LabelSettingsDisableWatcherHelp": "מבטל את הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", - "LabelSettingsEnableWatcher": "הפעל צופה", - "LabelSettingsEnableWatcherForLibrary": "הפעל צופה תיקייה עבור ספרייה", + "LabelSettingsEnableWatcher": "הפעל עוקב", + "LabelSettingsEnableWatcherForLibrary": "הפעל עוקב תיקייה עבור ספרייה", "LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", "LabelSettingsExperimentalFeatures": "תכונות ניסיוניות", "LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.", - "LabelSettingsFindCovers": "מצא כיסויים", - "LabelSettingsFindCoversHelp": "אם לספר השמע שלך אין כיסוי מוטמע או תמונת כיסוי בתיקייה, הסורק ינסה למצוא כיסוי.<br>שים לב: זה יאריך את זמן הסריקה", - "LabelSettingsHideSingleBookSeries": "הסתר סדרות ספר אחד", + "LabelSettingsFindCovers": "מצא כריכות", + "LabelSettingsFindCoversHelp": "אם לספר הקולי שלך אין כריכה מוטמעת או תמונת כריכה בתיקייה, הסורק ינסה למצוא תמונת כריכה.<br>שים לב: זה יאריך את זמן הסריקה", + "LabelSettingsHideSingleBookSeries": "הסתר סדרות עם ספר אחד", "LabelSettingsHideSingleBookSeriesHelp": "סדרות הכוללות ספר אחד יוסתרו מדף הסדרות ומדף הבית.", - "LabelSettingsHomePageBookshelfView": "השתמש בתצוגת מדף על דף הבית", + "LabelSettingsHomePageBookshelfView": "השתמש בתצוגת מדף בדף הבית", "LabelSettingsLibraryBookshelfView": "השתמש בתצוגת מדף בספרייה", - "LabelSettingsOnlyShowLaterBooksInContinueSeries": "דלג על ספרים קודמים בהמשך סדרות", - "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "מדף המשך סדרות מציג את הספר הראשון שלא התחיל בסדרה שיש לפחות ספר אחד שהושלם ואין ספרים בתהליך. הפעלת ההגדרה הזו תמשיך סדרות מהספר שהושלם הכי הרחק במקום מהספר הראשון שלא התחיל.", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "דלג על ספרים קודמים ב-המשך סדרה", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "מדף המשך סדרות מציג את הספר הראשון שלא הושמע בסדרה שיש בה לפחות ספר אחד שהושלם ואין ספרים שכבר באמצע שמיעה. הפעלת הגדרה זו תמשיך סדרות מהספר שהושלם הכי מתקדם בסדרה במקום מהספר הראשון שלא הושמע.", "LabelSettingsParseSubtitles": "פענח כתוביות", "LabelSettingsParseSubtitlesHelp": "העתק כותרת משנה משם תיקיית הספר.<br>כותרת המשנה חייבת להיות מופרדת עם התו ״-״<br>לדוגמא, כותרת המשנה לספר ״שם הספר - כותרת משנה״, היא ״כותרת משנה״", - "LabelSettingsPreferMatchedMetadata": "עדיף מטה-נתונים מתואמים", - "LabelSettingsPreferMatchedMetadataHelp": "נתונים מתואמים ידריך פרטי פריט כאשר משתמשים בהתאמה מהירה. כברירת מחדל, התאמה מהירה תמלא פרטים חסרים בלבד.", + "LabelSettingsPreferMatchedMetadata": "העדף מטה-נתונים מותאמים", + "LabelSettingsPreferMatchedMetadataHelp": "נתונים מותאמים יועדפו על פני פרטים שכבר מוטמעים בפריט כאשר התאמה מהירה בשימוש. כברירת מחדל, התאמה מהירה תמלא פרטים חסרים בלבד.", "LabelSettingsSkipMatchingBooksWithASIN": "דלג על ספרים שכבר יש להם ASIN", "LabelSettingsSkipMatchingBooksWithISBN": "דלג על ספרים שכבר יש להם ISBN", "LabelSettingsSortingIgnorePrefixes": "התעלם מקידומות במיון", "LabelSettingsSortingIgnorePrefixesHelp": "לדוגמא, לקידומת ״ה״ שם הספר, שם הספר ימוין בתור ״שם הספר״, ״ה״", - "LabelSettingsSquareBookCovers": "השתמש בכיסויים מרובעים לספרים", - "LabelSettingsSquareBookCoversHelp": "מעדיף להשתמש בכיסויים מרובעים מעל כיסויים סטנדרטיים ביחס 1.6:1", - "LabelSettingsStoreCoversWithItem": "אחסן כיסויים עם פריט", + "LabelSettingsSquareBookCovers": "השתמש בכריכות מרובעות לספרים", + "LabelSettingsSquareBookCoversHelp": "השתמש בכריכות מרובעות על פני בכריכות סטנדרטיות ביחס 1.6:1", + "LabelSettingsStoreCoversWithItem": "אחסן תמונת כריכה עם הפריט", "LabelSettingsStoreCoversWithItemHelp": "כברירת מחדל, צילומי כריכות נשמרים בתיקיית /metadata/items, לאחר הפעלת הגדרה זו צילומי כריכות יישמרו בתיקיית הספר, רק קובץ אחד בשם ״cover״ יישמר", - "LabelSettingsStoreMetadataWithItem": "אחסן מטה-נתונים עם פריט", + "LabelSettingsStoreMetadataWithItem": "אחסן מטה-נתונים עם הפריט", "LabelSettingsStoreMetadataWithItemHelp": "כברירת מחדל, קבצי מטה-נתונים מאוחסנים ב- /metadata/items, הפעלת ההגדרה תאחסן קבצי מטה-נתונים בתיקיית פריט שלך בספרייה", "LabelSettingsTimeFormat": "פורמט זמן", "LabelShowAll": "הצג הכל", @@ -496,24 +496,24 @@ "LabelStatsBestDay": "היום הטוב ביותר", "LabelStatsDailyAverage": "ממוצע יומי", "LabelStatsDays": "ימים", - "LabelStatsDaysListened": "ימים שהוקשבו", + "LabelStatsDaysListened": "מספר ימים בהם נשמע ספר", "LabelStatsHours": "שעות", "LabelStatsInARow": "ברצף", - "LabelStatsItemsFinished": "פריטים שסיימו", + "LabelStatsItemsFinished": "פריטים שסיימת", "LabelStatsItemsInLibrary": "פריטים בספרייה", "LabelStatsMinutes": "דקות", "LabelStatsMinutesListening": "דקות האזנה", "LabelStatsOverallDays": "ימים כולל", "LabelStatsOverallHours": "שעות כולל", "LabelStatsWeekListening": "האזנה שבועית", - "LabelSubtitle": "תת כותרת", + "LabelSubtitle": "כותרת משנה", "LabelSupportedFileTypes": "סוגי קבצים נתמכים", "LabelTag": "תג", "LabelTags": "תגיות", "LabelTagsAccessibleToUser": "תגיות נגישות למשתמש", "LabelTagsNotAccessibleToUser": "תגיות לא נגישות למשתמש", "LabelTasks": "משימות פעילות", - "LabelTextEditorBulletedList": "רשימה עם נקודות", + "LabelTextEditorBulletedList": "רשימת נקודות", "LabelTextEditorLink": "קישור", "LabelTextEditorNumberedList": "רשימה ממוספרת", "LabelTextEditorUnlink": "ביטול קישור", @@ -527,100 +527,100 @@ "LabelTimeToShift": "זמן להיסט בשניות", "LabelTitle": "כותרת", "LabelToolsEmbedMetadata": "הטמעת מטה-נתונים", - "LabelToolsEmbedMetadataDescription": "הטמעת מטה-נתונים לקבצי שמע כולל תמונות שער ופרקים.", + "LabelToolsEmbedMetadataDescription": "הטמעת מטה-נתונים לקבצי שמע כולל תמונות כריכה ופרקים.", "LabelToolsMakeM4b": "יצירת קובץ אודיו M4B", "LabelToolsMakeM4bDescription": "יצירת קובץ אודיו .M4B עם מטה-נתונים מוטמעים, תמונת שער ופרקים.", "LabelToolsSplitM4b": "פיצול M4B ל-MP3", "LabelToolsSplitM4bDescription": "יצירת קבצי MP3 מ-M4B מפוצל לפי פרקים עם מטה-נתונים מוטמעים, תמונת שער ופרקים.", "LabelTotalDuration": "משך כולל", - "LabelTotalTimeListened": "סך הזמן שהוקשב", - "LabelTrackFromFilename": "מסלול משמות קבצים", - "LabelTrackFromMetadata": "מסלול ממטה-נתונים", - "LabelTracks": "מסלולים", - "LabelTracksMultiTrack": "מסלול רב-ערוצים", - "LabelTracksNone": "אין מסלולים", - "LabelTracksSingleTrack": "מסלול יחיד", + "LabelTotalTimeListened": "סך הזמן שהקשבת", + "LabelTrackFromFilename": "רצועות משמות קבצים", + "LabelTrackFromMetadata": "רצועות ממטה-נתונים", + "LabelTracks": "רצועות", + "LabelTracksMultiTrack": "רב-ערוצי", + "LabelTracksNone": "אין ערוצים", + "LabelTracksSingleTrack": "רצועה יחידה", "LabelType": "סוג", "LabelUnabridged": "לא מקוצר", "LabelUndo": "בטל", "LabelUnknown": "לא ידוע", "LabelUpdateCover": "עדכן כריכה", - "LabelUpdateCoverHelp": "אפשר כיסוי מעלה של כריכות קיימות עבור הספרים הנבחרים כאשר נמצאה התאמה", - "LabelUpdatedAt": "עודכן ב", + "LabelUpdateCoverHelp": "אפשר החלפה של כריכות קיימות עבור הספרים הנבחרים כאשר נמצאה התאמה", + "LabelUpdatedAt": "עודכן ב-", "LabelUpdateDetails": "עדכון פרטים", - "LabelUpdateDetailsHelp": "אפשר עדכון מעלה של פרטים קיימים עבור הספרים הנבחרים כאשר נמצאה התאמה", + "LabelUpdateDetailsHelp": "אפשר החלפה של פרטים קיימים עבור הספרים הנבחרים כאשר נמצאה התאמה", "LabelUploaderDragAndDrop": "גרור ושחרר קבצים או תיקיות", "LabelUploaderDropFiles": "שחרר קבצים", "LabelUploaderItemFetchMetadataHelp": "משיכת כותרת, סופר וסדרה באופן אוטומטי", - "LabelUseChapterTrack": "השתמש במסלול פרקים", - "LabelUseFullTrack": "השתמש במסלול מלא", + "LabelUseChapterTrack": "השתמש ברצועות הפרקים", + "LabelUseFullTrack": "השתמש ברצועה המלאה", "LabelUser": "משתמש", "LabelUsername": "שם משתמש", "LabelValue": "ערך", "LabelVersion": "גרסה", "LabelViewBookmarks": "הצג סימניות", "LabelViewChapters": "הצג פרקים", - "LabelViewQueue": "הצג תור הנגן", + "LabelViewQueue": "הצג תור נגן", "LabelVolume": "עוצמת קול", "LabelWeekdaysToRun": "ימי השבוע להרצה", "LabelYearReviewHide": "הסתר שנת סקירה", - "LabelYearReviewShow": "ראה שנת סקירה", - "LabelYourAudiobookDuration": "משך האודיובוק שלך", + "LabelYearReviewShow": "הצג שנת סקירה", + "LabelYourAudiobookDuration": "משך הספר הקולי שלך", "LabelYourBookmarks": "הסימניות שלך", "LabelYourPlaylists": "הפלייליסטים שלך", "LabelYourProgress": "ההתקדמות שלך", "MessageAddToPlayerQueue": "הוסף לתור הנגן", "MessageAppriseDescription": "כדי להשתמש בתכונה זו יש לך להריץ מופע של <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">ממשק התכנית האפליקציה</a> או API שיטפל בבקשות אלו. <br /> כתובת URL של ממשק ה-Apprise API צריכה להיות הנתיב המלא לשליחת ההתראה, לדוגמה, אם המופע של ה-API שלך מוצע ב-<code>http://192.168.1.1:8337</code> אז עליך לשים <code>http://192.168.1.1:8337/notify</code>.", "MessageBackupsDescription": "גיבויים כוללים משתמשים, התקדמות משתמש, פרטי פריטי ספרייה, הגדרות שרת ותמונות השמורות ב-<code>/metadata/items</code> & <code>/metadata/authors</code>. גיבויים <strong>לא</strong> כוללים קבצים שמורים בתיקיות הספרייה שלך.", - "MessageBatchQuickMatchDescription": "התאמה מהירה תנסה להוסיף כריכות ומטה-נתונים חסרים עבור הפריטים הנבחרים. הפעל את האפשרויות למטה כדי לאפשר להתאמה מהירה לדרוס כריכות קיימות ו/או מטה-נתונים.", - "MessageBookshelfNoCollections": "עדיין לא יצרת שום אוספים", - "MessageBookshelfNoResultsForFilter": "אין תוצאות עבור מסנן \"{0}: {1}\"", - "MessageBookshelfNoRSSFeeds": "אין RSS feeds פתוחים", + "MessageBatchQuickMatchDescription": "התאמה מהירה תנסה להוסיף כריכות ומטה-נתונים חסרים עבור הפריטים הנבחרים. הפעל את האפשרויות למטה כדי לאפשר להתאמה מהירה להחליף כריכות קיימות ו/או מטה-נתונים.", + "MessageBookshelfNoCollections": "עדיין לא יצרת אוספים", + "MessageBookshelfNoResultsForFilter": "אין תוצאות עבור סינון \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "אין ערוצי RSS פתוחים", "MessageBookshelfNoSeries": "אין לך סדרות", - "MessageChapterEndIsAfter": "סיום הפרק מאוחר מסיום האודיובוק שלך", + "MessageChapterEndIsAfter": "זמן סיום הפרק אחרי סיום הספר הקולי שלך", "MessageChapterErrorFirstNotZero": "הפרק הראשון חייב להתחיל ב-0", - "MessageChapterErrorStartGteDuration": "זמן התחלה לא תקין חייב להיות פחות ממשך האודיובוק", - "MessageChapterErrorStartLtPrev": "זמן התחלה לא תקין חייב להיות גדול או שווה לזמן התחלה של הפרק הקודם", - "MessageChapterStartIsAfter": "התחלת הפרק מאוחרת מסיום האודיובוק שלך", + "MessageChapterErrorStartGteDuration": "זמן התחלה לא תקין, חייב להיות פחות ממשך הספר הקולי", + "MessageChapterErrorStartLtPrev": "זמן התחלה לא תקין, חייב להיות גדול או שווה לזמן ההתחלה של הפרק הקודם", + "MessageChapterStartIsAfter": "התחלת הפרק אחרי סיום הספר הקולי שלך", "MessageCheckingCron": "בודק את תזמון העבודה...", - "MessageConfirmCloseFeed": "האם אתה בטוח שאתה רוצה לסגור את הפיד הזה?", + "MessageConfirmCloseFeed": "האם אתה בטוח שאתה רוצה לסגור את הערוץ הזה?", "MessageConfirmDeleteBackup": "האם אתה בטוח שברצונך למחוק גיבוי עבור {0}?", - "MessageConfirmDeleteFile": "זה ימחק את הקובץ מהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteFile": "הקובץ ימחק לצמיתות מהמערכת שלך. האם אתה בטוח?", "MessageConfirmDeleteLibrary": "האם אתה בטוח שברצונך למחוק לצמיתות את הספרייה \"{0}\"?", - "MessageConfirmDeleteLibraryItem": "זה ימחק את פריט הספרייה ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", - "MessageConfirmDeleteLibraryItems": "זה ימחק {0} פריטי ספרייה ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteLibraryItem": "פריט הספרייה יימחק לצמיתות ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteLibraryItems": "פריטי הספרייה {0} יימחקו ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", "MessageConfirmDeleteSession": "האם אתה בטוח שאתה רוצה למחוק את ההפעלה הזו?", - "MessageConfirmForceReScan": "האם אתה בטוח שאתה רוצה לכוון איתור מחדש?", - "MessageConfirmMarkAllEpisodesFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כסיום?", - "MessageConfirmMarkAllEpisodesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כלא סיום?", - "MessageConfirmMarkSeriesFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כסיום?", - "MessageConfirmMarkSeriesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כלא סיום?", + "MessageConfirmForceReScan": "האם אתה בטוח שאתה רוצה להכריח סריקה מחדש?", + "MessageConfirmMarkAllEpisodesFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כהסתיימו?", + "MessageConfirmMarkAllEpisodesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כלא הסתיימו?", + "MessageConfirmMarkSeriesFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כהסתיימו?", + "MessageConfirmMarkSeriesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כלא הסתיימו?", "MessageConfirmQuickEmbed": "אזהרה! הטמעה מהירה לא תגבה גיבוי של קבצי האודיו שלך. וודא שיש לך גיבוי של קבצי האודיו שלך. <br><br>האם ברצונך להמשיך?", "MessageConfirmRemoveAllChapters": "האם אתה בטוח שברצונך להסיר את כל הפרקים?", "MessageConfirmRemoveAuthor": "האם אתה בטוח שברצונך להסיר את המחבר \"{0}\"?", "MessageConfirmRemoveCollection": "האם אתה בטוח שברצונך להסיר אוסף \"{0}\"?", "MessageConfirmRemoveEpisode": "האם אתה בטוח שברצונך להסיר פרק \"{0}\"?", "MessageConfirmRemoveEpisodes": "האם אתה בטוח שברצונך להסיר {0} פרקים?", - "MessageConfirmRemoveListeningSessions": "האם אתה בטוח שברצונך להסיר {0} מפגשי האזנה?", - "MessageConfirmRemoveNarrator": "האם אתה בטוח שברצונך להסיר נרטור \"{0}\"?", + "MessageConfirmRemoveListeningSessions": "האם אתה בטוח שברצונך להסיר {0} הפעלות האזנה?", + "MessageConfirmRemoveNarrator": "האם אתה בטוח שברצונך להסיר מקריא \"{0}\"?", "MessageConfirmRemovePlaylist": "האם אתה בטוח שברצונך להסיר את רשימת ההשמעה שלך \"{0}\"?", - "MessageConfirmRenameGenre": "האם אתה בטוח שברצונך לשנות את שם הג'אנר \"{0}\" ל \"{1}\" עבור כל הפריטים?", - "MessageConfirmRenameGenreMergeNote": "הערה: ג'אנר זה כבר קיים ולכן הם יתמזגו.", - "MessageConfirmRenameGenreWarning": "אזהרה! יש ג'אנר דומה עם רישום שונה הכבר קיים \"{0}\".", + "MessageConfirmRenameGenre": "האם אתה בטוח שברצונך לשנות את שם הז'אנר \"{0}\" ל \"{1}\" עבור כל הפריטים?", + "MessageConfirmRenameGenreMergeNote": "הערה: ז'אנר זה כבר קיים ולכן הם יתמזגו.", + "MessageConfirmRenameGenreWarning": "אזהרה! יש ז'אנר דומה עם רישום שונה שכבר קיים \"{0}\".", "MessageConfirmRenameTag": "האם אתה בטוח שברצונך לשנות את שם התג \"{0}\" ל \"{1}\" עבור כל הפריטים?", "MessageConfirmRenameTagMergeNote": "הערה: התג זה כבר קיים ולכן הם יתמזגו.", - "MessageConfirmRenameTagWarning": "אזהרה! יש תג דומה עם רישום שונה הכבר קיים \"{0}\".", + "MessageConfirmRenameTagWarning": "אזהרה! יש תג דומה עם רישום שונה שכבר קיים \"{0}\".", "MessageConfirmReScanLibraryItems": "האם אתה בטוח שברצונך לסרוק מחדש {0} פריטים?", - "MessageConfirmSendEbookToDevice": "האם אתה בטוח שברצונך לשלוח {0} איבוק \"{1}\" למכשיר \"{2}\"?", + "MessageConfirmSendEbookToDevice": "האם אתה בטוח שברצונך לשלוח {0} את הספר האלקטרוני \"{1}\" למכשיר \"{2}\"?", "MessageDownloadingEpisode": "מוריד פרק", - "MessageDragFilesIntoTrackOrder": "גרור קבצים לסדר השמעה נכון", + "MessageDragFilesIntoTrackOrder": "גרור קבצים לסדר ההשמעה נכון", "MessageEmbedFinished": "ההטמעה הושלמה!", "MessageEpisodesQueuedForDownload": "{0} פרקים בתור להורדה", "MessageFeedURLWillBe": "כתובת URL של העדכון תהיה {0}", - "MessageFetching": "מבצע גישה...", - "MessageForceReScanDescription": "יסרוק את כל הקבצים מחדש כמו סריקה חדשה. תגיות ID3 של קבצי שמע, קבצי OPF וקבצי טקסט יסרקו כחדשים.", + "MessageFetching": "מושך...", + "MessageForceReScanDescription": "תבוצע סריקה מחדש כמו סריקה חדש מאפס, תגי ID3 של קבצי קול, קבצי OPF, וקבצי טקסט ייסרקו כחדשים.", "MessageImportantNotice": "הודעה חשובה!", - "MessageInsertChapterBelow": "הוסף פרק למטה", + "MessageInsertChapterBelow": "הוסף פרק מתחת", "MessageItemsSelected": "{0} פריטים נבחרו", "MessageItemsUpdated": "{0} פריטים עודכנו", "MessageJoinUsOn": "הצטרף אלינו ב-", @@ -629,12 +629,12 @@ "MessageLoadingFolders": "טוען תיקיות...", "MessageM4BFailed": "M4B נכשל!", "MessageM4BFinished": "M4B הושלם!", - "MessageMapChapterTitles": "מפה שמות פרקים לפרקי הספר השמורים שלך ללא שינוי תגים זמניים", + "MessageMapChapterTitles": "מפה שמות פרקים לפרקי הספר השמורים שלך ללא שינוי תגי זמן", "MessageMarkAllEpisodesFinished": "סמן את כל הפרקים כהסתיימו", "MessageMarkAllEpisodesNotFinished": "סמן את כל הפרקים כלא הסתיימו", "MessageMarkAsFinished": "סמן כהסתיים", "MessageMarkAsNotFinished": "סמן כלא הסתיים", - "MessageMatchBooksDescription": "ינסה להתאים ספרים בספריית הספרים שלך עם ספר מספק החיפוש הנבחר וימלא פרטים ריקים ותמונות כריכה. לא ימחק פרטים.", + "MessageMatchBooksDescription": "ינסה להתאים ספרים בספריית הספרים שלך עם ספר מספק החיפוש הנבחר וימלא פרטים ריקים ותמונות כריכה. לא יחליף פרטים קיימים.", "MessageNoAudioTracks": "אין רצועות שמע", "MessageNoAuthors": "אין סופרים", "MessageNoBackups": "אין גיבויים", @@ -649,12 +649,12 @@ "MessageNoEpisodes": "אין פרקים", "MessageNoFoldersAvailable": "אין תיקיות זמינות", "MessageNoGenres": "אין ז'אנרים", - "MessageNoIssues": "אין בעיות", + "MessageNoIssues": "אין תקלות", "MessageNoItems": "אין פריטים", "MessageNoItemsFound": "לא נמצאו פריטים", - "MessageNoListeningSessions": "אין מפגשי האזנה", + "MessageNoListeningSessions": "אין הפעלות האזנה", "MessageNoLogs": "אין לוגים", - "MessageNoMediaProgress": "אין התקדמות מדיה", + "MessageNoMediaProgress": "אין התקדמות במדיה", "MessageNoNotifications": "אין התראות", "MessageNoPodcastsFound": "לא נמצאו פודקאסטים", "MessageNoResults": "אין תוצאות", @@ -664,14 +664,14 @@ "MessageNoTasksRunning": "אין משימות פעילות", "MessageNotYetImplemented": "עדיין לא מיושם", "MessageNoUpdateNecessary": "לא נדרש עדכון", - "MessageNoUpdatesWereNecessary": "לא הייתה צורך בעדכונים", + "MessageNoUpdatesWereNecessary": "לא היה צורך בעדכונים", "MessageNoUserPlaylists": "אין לך רשימות השמעה", "MessageOr": "או", "MessagePauseChapter": "השהה השמעת הפרק", - "MessagePlayChapter": "השמע לתחילת הפרק", + "MessagePlayChapter": "הקשב לתחילת הפרק", "MessagePlaylistCreateFromCollection": "צור רשימת השמעה מאוסף", - "MessagePodcastHasNoRSSFeedForMatching": "הפודקאסט אין לו כתובת URL של הזנת RSS לשימוש בהתאמה", - "MessageQuickMatchDescription": "ממלא פרטים ריקים וכריכות עם התוצאה הראשונה מ '{0}'. לא ימחק פרטים אלא אם הגדרות השרת 'מעדיפים מטה-נתונים התואמים' מופעלות.", + "MessagePodcastHasNoRSSFeedForMatching": "לפודקאסט אין כתובת URL של ערוץ RSS להתאמה", + "MessageQuickMatchDescription": "ממלא פרטים ריקים וכריכות עם התוצאה הראשונה מ '{0}'. לא ימחק פרטים אלא אם הגדרת השרת 'העדף מטה-נתונים מותאמים' מופעלת.", "MessageRemoveChapter": "הסר פרק", "MessageRemoveEpisodes": "הסר {0} פרקים", "MessageRemoveFromPlayerQueue": "הסר מתור ההשמעה של הנגן", @@ -679,29 +679,29 @@ "MessageReportBugsAndContribute": "דווח על באגים, בקש תכונות חדשות, ותרום ב-", "MessageResetChaptersConfirm": "האם אתה בטוח שברצונך לאפס את הפרקים ולבטל את השינויים שביצעת?", "MessageRestoreBackupConfirm": "האם אתה בטוח שברצונך לשחזר את הגיבוי שנוצר ב", - "MessageRestoreBackupWarning": "שחזור גיבוי ימחק את כל מסד הנתונים השוכן ב /config ואת תמונות הכריכה ב- /metadata/items & /metadata/authors.<br /><br />גיבויים אינם משנים קבצים בתיקיות הספרייה שלך. אם הגדרות השרת מופעלות כדי לאחסן תמונות כריכה ומטא-נתונים בתיקיות הספרייה שלך אז אלה לא יגובו או ימחקו.<br /><br />כל הלקוחות המשתמשים בשרת שלך יתעדכנו באופן אוטומטי.", + "MessageRestoreBackupWarning": "שחזור גיבוי ימחק את כל מסד הנתונים הנוכחי השוכן ב /config ואת תמונות הכריכה ב- /metadata/items & /metadata/authors.<br /><br />גיבויים אינם משנים קבצים בתיקיות הספרייה שלך. אם הגדרות השרת לאחסן תמונות כריכה ומטא-נתונים בתיקיות הספרייה שלך מופעלות אז אלה לא יגובו או ימחקו.<br /><br />כל האפליקציות המשתמשות בשרת שלך יתעדכנו באופן אוטומטי.", "MessageSearchResultsFor": "תוצאות חיפוש עבור", "MessageSelected": "{0} נבחרו", "MessageServerCouldNotBeReached": "לא ניתן להגיע אל השרת", "MessageSetChaptersFromTracksDescription": "קבע פרקים באמצעות כל קובץ שמע כפרק וכותרת פרק כשם הקובץ שמע", - "MessageStartPlaybackAtTime": "האם אתה בטוח שברצונך להתחיל השמעה עבור \"{0}\" ב-{1}?", + "MessageStartPlaybackAtTime": "להתחיל השמעה עבור \"{0}\" ב-{1}?", "MessageThinking": "חושב...", "MessageUploaderItemFailed": "העלאת הפריט נכשלה", "MessageUploaderItemSuccess": "העלאה הצליחה!", "MessageUploading": "מעלה...", "MessageValidCronExpression": "ביטוי Cron חוקי", - "MessageWatcherIsDisabledGlobally": "שומר מנוטרל באופן גלובלי בהגדרות השרת", + "MessageWatcherIsDisabledGlobally": "עוקב מנוטרל באופן גלובלי בהגדרות השרת", "MessageXLibraryIsEmpty": "ספריית {0} ריקה!", - "MessageYourAudiobookDurationIsLonger": "הזמן של הספר השמע שלך ארוך יותר מהזמן שנמצא", - "MessageYourAudiobookDurationIsShorter": "הזמן של הספר השמע שלך קצר יותר מהזמן שנמצא", - "NoteChangeRootPassword": "המשתמש השורש הוא המשתמש היחיד שיכול להיות לו סיסמה ריקה", - "NoteChapterEditorTimes": "הערה: זמן ההתחלה של הפרק הראשון חייב להישאר 0:00 וזמן ההתחלה של הפרק האחרון לא יכול לחרוג מהזמן של ספר השמע הזה.", - "NoteFolderPicker": "הערה: תיקיות שכבר מוגדרות לא יוצגו", - "NoteRSSFeedPodcastAppsHttps": "אזהרה: רוב יישומי הפודקאסט דורשים שהכתובת URL של העדכון תשתמש ב HTTPS", - "NoteRSSFeedPodcastAppsPubDate": "אזהרה: פרק(ים) אחד או יותר לא מכילים תאריך פרסום. חלק מיישומי הפודקאסט דורשים זאת.", + "MessageYourAudiobookDurationIsLonger": "הזמן של הספר הקולי שלך ארוך יותר מהזמן שנמצא", + "MessageYourAudiobookDurationIsShorter": "הזמן של הספר הקולי שלך קצר יותר מהזמן שנמצא", + "NoteChangeRootPassword": "המשתמש root הוא המשתמש היחיד שיכולה להיות לו סיסמה ריקה", + "NoteChapterEditorTimes": "הערה: זמן ההתחלה של הפרק הראשון חייב להישאר 0:00 וזמן ההתחלה של הפרק האחרון לא יכול לחרוג מהזמן של ספר השמע.", + "NoteFolderPicker": "הערה: תיקיות שכבר מופו לא יוצגו", + "NoteRSSFeedPodcastAppsHttps": "אזהרה: רוב יישומי הפודקאסט דורשים שכתובת ה-URL ערוץ ה-RSS תשתמש ב-HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "אזהרה: פרק אחד או יותר לא מכילים תאריך פרסום. חלק מיישומי הפודקאסט דורשים זאת.", "NoteUploaderFoldersWithMediaFiles": "תיקיות עם קבצי מדיה יעובדו כפריטי ספריה נפרדים.", - "NoteUploaderOnlyAudioFiles": "אם מעלים רק קבצי שמע, כל קובץ שמע יעובד כספר שמע נפרד.", - "NoteUploaderUnsupportedFiles": "קבצים לא נתמכים מתעלמים. בעת בחירת תיקייה או זריקתה, קבצים אחרים שאינם בתיקיית פריט מתעלמים.", + "NoteUploaderOnlyAudioFiles": "אם מועלים רק קבצי שמע, כל קובץ שמע יעובד כספר שמע נפרד.", + "NoteUploaderUnsupportedFiles": "מתעלם מקבצים לא נתמכים. בעת בחירת תיקייה או גרירה לדף, מתעלם מקבצים אחרים שאינם בתיקיית פריט.", "PlaceholderNewCollection": "שם אוסף חדש", "PlaceholderNewFolderPath": "נתיב תיקייה חדשה", "PlaceholderNewPlaylist": "שם רשימת השמעה חדשה", @@ -712,7 +712,7 @@ "ToastAuthorImageRemoveFailed": "הסרת התמונה של המחבר נכשלה", "ToastAuthorImageRemoveSuccess": "תמונת המחבר הוסרה בהצלחה", "ToastAuthorUpdateFailed": "עדכון המחבר נכשל", - "ToastAuthorUpdateMerged": "המחבר ממוזג", + "ToastAuthorUpdateMerged": "המחבר מוזג", "ToastAuthorUpdateSuccess": "המחבר עודכן בהצלחה", "ToastAuthorUpdateSuccessNoImageFound": "המחבר עודכן (תמונה לא נמצאה)", "ToastBackupCreateFailed": "יצירת גיבוי נכשלה", @@ -722,28 +722,28 @@ "ToastBackupRestoreFailed": "שחזור הגיבוי נכשל", "ToastBackupUploadFailed": "העלאת הגיבוי נכשלה", "ToastBackupUploadSuccess": "הגיבוי הועלה בהצלחה", - "ToastBatchUpdateFailed": "עדכון הצטברות נכשל", - "ToastBatchUpdateSuccess": "הצטברות עודכנה בהצלחה", + "ToastBatchUpdateFailed": "עדכון קבוצתי נכשל", + "ToastBatchUpdateSuccess": "עדכון קבוצתי הצליח", "ToastBookmarkCreateFailed": "יצירת סימניה נכשלה", "ToastBookmarkCreateSuccess": "הסימניה נוספה בהצלחה", "ToastBookmarkRemoveFailed": "הסרת הסימניה נכשלה", "ToastBookmarkRemoveSuccess": "הסימניה הוסרה בהצלחה", "ToastBookmarkUpdateFailed": "עדכון הסימניה נכשל", "ToastBookmarkUpdateSuccess": "הסימניה עודכנה בהצלחה", - "ToastChaptersHaveErrors": "הפרקים מכילים שגיאות", - "ToastChaptersMustHaveTitles": "הפרקים חייבים לכלול כותרות", + "ToastChaptersHaveErrors": "פרקים מכילים שגיאות", + "ToastChaptersMustHaveTitles": "פרקים חייבים לכלול כותרות", "ToastCollectionItemsRemoveFailed": "הסרת הפריט(ים) מהאוסף נכשלה", "ToastCollectionItemsRemoveSuccess": "הפריט(ים) הוסרו מהאוסף בהצלחה", "ToastCollectionRemoveFailed": "מחיקת האוסף נכשלה", "ToastCollectionRemoveSuccess": "האוסף הוסר בהצלחה", "ToastCollectionUpdateFailed": "עדכון האוסף נכשל", "ToastCollectionUpdateSuccess": "האוסף עודכן בהצלחה", - "ToastItemCoverUpdateFailed": "עדכון כיסוי הפריט נכשל", - "ToastItemCoverUpdateSuccess": "כיסוי הפריט עודכן בהצלחה", + "ToastItemCoverUpdateFailed": "עדכון כריכת הפריט נכשל", + "ToastItemCoverUpdateSuccess": "כריכת הפריט עודכנה בהצלחה", "ToastItemDetailsUpdateFailed": "עדכון פרטי הפריט נכשל", "ToastItemDetailsUpdateSuccess": "פרטי הפריט עודכנו בהצלחה", "ToastItemDetailsUpdateUnneeded": "לא נדרשים עדכונים לפרטי הפריט", - "ToastItemMarkedAsFinishedFailed": "סימון כפריט הושלם נכשל", + "ToastItemMarkedAsFinishedFailed": "סימון כפריט כהושלם נכשל", "ToastItemMarkedAsFinishedSuccess": "הפריט סומן כהושלם בהצלחה", "ToastItemMarkedAsNotFinishedFailed": "סימון כפריט שלא הושלם נכשל", "ToastItemMarkedAsNotFinishedSuccess": "הפריט סומן כלא הושלם בהצלחה", @@ -765,16 +765,16 @@ "ToastPodcastCreateSuccess": "הפודקאסט נוצר בהצלחה", "ToastRemoveItemFromCollectionFailed": "הסרת הפריט מהאוסף נכשלה", "ToastRemoveItemFromCollectionSuccess": "הפריט הוסר מהאוסף בהצלחה", - "ToastRSSFeedCloseFailed": "סגירת הערוץ RSS נכשלה", - "ToastRSSFeedCloseSuccess": "הערוץ RSS נסגר בהצלחה", + "ToastRSSFeedCloseFailed": "סגירת ערוץ ה-RSS נכשלה", + "ToastRSSFeedCloseSuccess": "ערוץ ה-RSS נסגר בהצלחה", "ToastSendEbookToDeviceFailed": "שליחת הספר אל המכשיר נכשלה", "ToastSendEbookToDeviceSuccess": "הספר נשלח אל המכשיר \"{0}\"", "ToastSeriesUpdateFailed": "עדכון הסדרה נכשל", "ToastSeriesUpdateSuccess": "הסדרה עודכנה בהצלחה", "ToastSessionDeleteFailed": "מחיקת הפעולה נכשלה", "ToastSessionDeleteSuccess": "הפעולה נמחקה בהצלחה", - "ToastSocketConnected": "חיבור קצה התקשורת בוצע בהצלחה", - "ToastSocketDisconnected": "התנתקות קצה התקשורת בוצעה", + "ToastSocketConnected": "קצה תקשורת חובר", + "ToastSocketDisconnected": "קצה תקשורת נותק", "ToastSocketFailedToConnect": "התחברות קצה התקשורת נכשלה", "ToastUserDeleteFailed": "מחיקת המשתמש נכשלה", "ToastUserDeleteSuccess": "המשתמש נמחק בהצלחה" From 8f80948211c203b2c966ca360b76f693368bee00 Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Sun, 17 Mar 2024 19:09:16 -0300 Subject: [PATCH 0461/2145] [PT-BR] Continue Series --- client/strings/pt-br.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 7a5a1eec..c4d00eb7 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -466,8 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Séries com um só livro serão ocultadas na página de séries e na prateleira de séries na página principal.", "LabelSettingsHomePageBookshelfView": "Usar visão estante na página principal", "LabelSettingsLibraryBookshelfView": "Usar visão estante na página da biblioteca", - "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", - "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Pular livros anteriores em Continuar Série", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "A prateleira Continuar Série na página principal de exibe o primeiro livro não iniciado em uma série que tem pelo menos um livro concluído e nenhum livro em andamento. Ativar essa configuração irá continuar a série a partir do livro mais recentemente concluído ao invés do primeiro livro não iniciado.", "LabelSettingsParseSubtitles": "Analisar subtítulos", "LabelSettingsParseSubtitlesHelp": "Extrair subtítulos do nome da pasta do audiobook.<br>Subtítulo deve estar separado por \" - \"<br>ex: \"Título do Livro - Um Subtítulo Aqui\" tem o subtítulo \"Um Subtítulo Aqui\"", "LabelSettingsPreferMatchedMetadata": "Preferir metadados consultados", @@ -778,4 +778,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} \ No newline at end of file +} From 5b836dfa288928af31476f418f0ca06bd9b60c9b Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 17 Mar 2024 19:19:52 -0500 Subject: [PATCH 0462/2145] Remove duplicate he language code --- client/plugins/i18n.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index e35b7e7c..0ee0e5b1 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -25,8 +25,7 @@ const languageCodeMap = { 'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, - 'zh-tw': { label: '正體中文 (Traditional Chinese)', dateFnsLocale: 'zhTW' }, - 'he': { label: 'עברית', dateFnsLocale: 'he' } + 'zh-tw': { label: '正體中文 (Traditional Chinese)', dateFnsLocale: 'zhTW' } } Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { return { From 56f1bfef507228ddeb58795666d6264bfe5cd966 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Tue, 19 Mar 2024 17:57:24 +0100 Subject: [PATCH 0463/2145] Auth/OpenID: Implement Permissions via OpenID * Ability to set group * Ability to set more advanced permissions * Modified TextInputWithLabel to provide an ability to specify a different placeholder then the name --- client/components/ui/TextInputWithLabel.vue | 3 +- client/pages/config/authentication.vue | 45 +++++++++++- server/Auth.js | 81 ++++++++++++++++++++- server/objects/settings/ServerSettings.js | 17 ++++- server/objects/user/User.js | 72 ++++++++++++++++++ 5 files changed, 210 insertions(+), 8 deletions(-) diff --git a/client/components/ui/TextInputWithLabel.vue b/client/components/ui/TextInputWithLabel.vue index 032e24ca..f653a18b 100644 --- a/client/components/ui/TextInputWithLabel.vue +++ b/client/components/ui/TextInputWithLabel.vue @@ -5,7 +5,7 @@ >{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em></label > </slot> - <ui-text-input :placeholder="label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" /> + <ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" /> </div> </template> @@ -14,6 +14,7 @@ export default { props: { value: [String, Number], label: String, + placeholder: String, note: String, type: { type: String, diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 3373e287..91c6cfe2 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -70,17 +70,42 @@ <p class="pl-4 text-sm text-gray-300 mt-5">{{ $strings.LabelMatchExistingUsersByDescription }}</p> </div> - <div class="flex items-center py-4 px-1"> + <div class="flex items-center py-4 px-1 w-full"> <ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" /> <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"> + <div class="flex items-center py-4 px-1 w-full"> <ui-toggle-switch labeledBy="auto-register-toggle" v-model="newAuthSettings.authOpenIDAutoRegister" :disabled="savingSettings" /> <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 class="flex items-center pt-6 pb-1 px-1 w-full">Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.</div> + <div class="flex items-center mb-2"> + <div class="w-96"> + <ui-text-input-with-label ref="openidGroupClaim" v-model="newAuthSettings.authOpenIDGroupClaim" :disabled="savingSettings" :placeholder="'groups'" :label="'Group Claim'" /> + </div> + <p class="pl-4 text-sm text-gray-300 mt-5"> + Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to + multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied. + </p> + </div> + + <div class="flex mb-2"> + <div class="w-96 pt-6"> + <ui-text-input-with-label ref="openidAdvancedPermsClaim" v-model="newAuthSettings.authOpenIDAdvancedPermsClaim" :disabled="savingSettings" :placeholder="'abspermissions'" :label="'Advanced Permission Claim'" /> + </div> + <div class="pl-4 text-sm text-gray-300 mt-5 flex-column"> + <p class=""> + Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure: + </p> + <pre class="text-pre-wrap mt-2" + >{{ newAuthSettings.authOpenIDSamplePermissions }} + </pre> + </div> + </div> </div> </transition> </div> @@ -222,6 +247,22 @@ export default { } }) } + + function isValidClaim(claim) { + if (claim === '') return true + + const pattern = new RegExp('^[a-zA-Z][a-zA-Z0-9_-]*$', 'i') + return pattern.test(claim) + } + if (!isValidClaim(this.newAuthSettings.authOpenIDGroupClaim)) { + this.$toast.error('Group Claim: Invalid claim name') + isValid = false + } + if (!isValidClaim(this.newAuthSettings.authOpenIDAdvancedPermsClaim)) { + this.$toast.error('Advanced Permission Claim: Invalid claim name') + isValid = false + } + return isValid }, async saveSettings() { diff --git a/server/Auth.js b/server/Auth.js index 352faf66..a4cdd1fc 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -98,7 +98,7 @@ class Auth { scope: 'openid profile email' } }, async (tokenset, userinfo, done) => { - Logger.debug(`[Auth] openid callback userinfo=`, userinfo) + Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2)) let failureMessage = 'Unauthorized' if (!userinfo.sub) { @@ -106,6 +106,35 @@ class Auth { return done(null, null, failureMessage) } + // Check if the claims itself are returned correctly + const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; + if (groupClaimName) { + if (!userinfo[groupClaimName]) { + Logger.error(`[Auth] openid callback invalid: Group claim ${groupClaimName} configured, but not found or empty in userinfo`) + return done(null, null, failureMessage) + } + + const groupsList = userinfo[groupClaimName] + const targetRoles = ['admin', 'user', 'guest'] + + // Convert the list to lowercase for case-insensitive comparison + const groupsListLowercase = groupsList.map(group => group.toLowerCase()) + + // Check if any of the target roles exist in the groups list + const containsTargetRole = targetRoles.some(role => groupsListLowercase.includes(role.toLowerCase())) + + if (!containsTargetRole) { + Logger.info(`[Auth] openid callback: Denying access because neither admin nor user or guest is included is inside the group claim. Groups found: `, groupsList) + return done(null, null, failureMessage) + } + } + + const advancedPermsClaimName = Database.serverSettings.authOpenIDAdvancedPermsClaim + if (advancedPermsClaimName && !userinfo[advancedPermsClaimName]) { + Logger.error(`[Auth] openid callback invalid: Advanced perms claim ${advancedPermsClaimName} configured, but not found or empty in userinfo`) + return done(null, null, failureMessage) + } + // First check for matching user by sub let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) if (!user) { @@ -157,6 +186,43 @@ class Auth { return } + // Set user group if name of groups claim is configured + if (groupClaimName) { + const groupsList = userinfo[groupClaimName] ? userinfo[groupClaimName].map(group => group.toLowerCase()) : [] + const rolesInOrderOfPriority = ['admin', 'user', 'guest'] + + let userType = null + + for (let role of rolesInOrderOfPriority) { + if (groupsList.includes(role)) { + userType = role // This will override with the highest priority role found + break // Stop searching once the highest priority role is found + } + } + + // Actually already checked above, but just to be sure + if (!userType) { + Logger.error(`[Auth] openid callback: Denying access because neither admin nor user or guest is included is inside the group claim. Groups found: `, groupsList) + return done(null, null, failureMessage) + } + + Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`) + user.type = userType + await Database.userModel.updateFromOld(user) + } + + if (advancedPermsClaimName) { + try { + Logger.debug(`[Auth] openid callback: Updating advanced perms for user ${user.username} to ${JSON.stringify(userinfo[advancedPermsClaimName])}`) + + user.updatePermissionsFromExternalJSON(userinfo[advancedPermsClaimName]) + await Database.userModel.updateFromOld(user) + } catch (error) { + Logger.error(`[Auth] openid callback: Error updating advanced perms for user, error: `, error) + return done(null, null, failureMessage) + } + } + // We also have to save the id_token for later (used for logout) because we cannot set cookies here user.openid_id_token = tokenset.id_token @@ -334,10 +400,19 @@ class Auth { sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback } + var scope = 'openid profile email' + if (global.ServerSettings.authOpenIDGroupClaim) { + scope += ' ' + global.ServerSettings.authOpenIDGroupClaim + } + if (global.ServerSettings.authOpenIDAdvancedPermsClaim) { + scope += ' ' + global.ServerSettings.authOpenIDAdvancedPermsClaim + } + const authorizationUrl = client.authorizationUrl({ ...oidcStrategy._params, state: state, response_type: 'code', + scope: scope, code_challenge, code_challenge_method }) @@ -424,12 +499,12 @@ class Auth { } function handleAuthError(isMobile, errorCode, errorMessage, logMessage, response) { - Logger.error(logMessage) + Logger.error(JSON.stringify(logMessage, null, 2)) 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() + '\n' + JSON.stringify(response.body, null, 2)) + Logger.debug(header + '\n' + JSON.stringify(response.body, null, 2)) } if (isMobile) { diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 5cc68a5c..5c2da381 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -1,6 +1,7 @@ const packageJson = require('../../../package.json') const { BookshelfView } = require('../../utils/constants') const Logger = require('../../Logger') +const User = require('../user/User') class ServerSettings { constructor(settings) { @@ -72,6 +73,8 @@ class ServerSettings { this.authOpenIDAutoRegister = false this.authOpenIDMatchExistingBy = null this.authOpenIDMobileRedirectURIs = ['audiobookshelf://oauth'] + this.authOpenIDGroupClaim = '' + this.authOpenIDAdvancedPermsClaim = '' if (settings) { this.construct(settings) @@ -129,6 +132,8 @@ class ServerSettings { this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null this.authOpenIDMobileRedirectURIs = settings.authOpenIDMobileRedirectURIs || ['audiobookshelf://oauth'] + this.authOpenIDGroupClaim = settings.authOpenIDGroupClaim || '' + this.authOpenIDAdvancedPermsClaim = settings.authOpenIDAdvancedPermsClaim || '' if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] @@ -216,7 +221,9 @@ class ServerSettings { authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, authOpenIDAutoRegister: this.authOpenIDAutoRegister, authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy, - authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client + authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs, // Do not return to client + authOpenIDGroupClaim: this.authOpenIDGroupClaim, // Do not return to client + authOpenIDAdvancedPermsClaim: this.authOpenIDAdvancedPermsClaim // Do not return to client } } @@ -226,6 +233,8 @@ class ServerSettings { delete json.authOpenIDClientID delete json.authOpenIDClientSecret delete json.authOpenIDMobileRedirectURIs + delete json.authOpenIDGroupClaim + delete json.authOpenIDAdvancedPermsClaim return json } @@ -262,7 +271,11 @@ class ServerSettings { authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, authOpenIDAutoRegister: this.authOpenIDAutoRegister, authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy, - authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client + authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs, // Do not return to client + authOpenIDGroupClaim: this.authOpenIDGroupClaim, // Do not return to client + authOpenIDAdvancedPermsClaim: this.authOpenIDAdvancedPermsClaim, // Do not return to client + + authOpenIDSamplePermissions: User.getSampleAbsPermissions() } } diff --git a/server/objects/user/User.js b/server/objects/user/User.js index d926e8be..d09e921d 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -268,6 +268,78 @@ class User { return hasUpdates } + // List of expected permission properties from the client + static permissionMapping = { + canDownload: 'download', + canUpload: 'upload', + canDelete: 'delete', + canUpdate: 'update', + canAccessExplicitContent: 'accessExplicitContent', + canAccessAllLibraries: 'accessAllLibraries', + canAccessAllTags: 'accessAllTags', + tagsAreBlacklist: 'selectedTagsNotAccessible', + // Direct mapping for array-based permissions + allowedLibraries: 'librariesAccessible', + allowedTags: 'itemTagsSelected', + } + + /** + * Update user from external JSON + * + * @param {object} absPermissions JSON containg user permissions + */ + updatePermissionsFromExternalJSON(absPermissions) { + // Initialize all permissions to false first + Object.keys(User.permissionMapping).forEach(mappingKey => { + const userPermKey = User.permissionMapping[mappingKey]; + if (typeof this.permissions[userPermKey] === 'boolean') { + this.permissions[userPermKey] = false; // Default to false for boolean permissions + } else { + this[userPermKey] = []; // Default to empty array for other properties + } + }); + + Object.keys(absPermissions).forEach(absKey => { + const userPermKey = User.permissionMapping[absKey] + if (!userPermKey) { + throw new Error(`Unexpected permission property: ${absKey}`) + } + + // Update the user's permissions based on absPermissions + this.permissions[userPermKey] = absPermissions[absKey] + }); + + // Handle allowedLibraries and allowedTags separately if needed + if (absPermissions.allowedLibraries) { + this.librariesAccessible = absPermissions.allowedLibraries + } + if (absPermissions.allowedTags) { + this.itemTagsSelected = absPermissions.allowedTags + } + } + + /** + * Get a sample to show how a JSON for updatePermissionsFromExternalJSON should look like + * + * @returns JSON string + */ + static getSampleAbsPermissions() { + // Start with a template object where all permissions are false for simplicity + const samplePermissions = Object.keys(User.permissionMapping).reduce((acc, key) => { + // For array-based permissions, provide a sample array + if (key === 'allowedLibraries') { + acc[key] = [`ExampleLibrary`, `AnotherLibrary`]; + } else if (key === 'allowedTags') { + acc[key] = [`ExampleTag`, `AnotherTag`, `ThirdTag`]; + } else { + acc[key] = false; + } + return acc; + }, {}); + + return JSON.stringify(samplePermissions, null, 2); // Pretty print the JSON + } + /** * Get first available library id for user * From 9511122bae10ae67dd2299961e35ea2e82c8f284 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Tue, 19 Mar 2024 19:28:26 +0200 Subject: [PATCH 0464/2145] Fix LibraryItem and Media file update logic for library scans --- server/scanner/BookScanner.js | 32 +++++++++++++++-- server/scanner/LibraryItemScanData.js | 49 +++++++++++++++++++++++---- server/scanner/PodcastScanner.js | 18 ++++++++-- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index c738c52e..c12441b2 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -20,6 +20,7 @@ const LibraryScan = require("./LibraryScan") const OpfFileScanner = require('./OpfFileScanner') const NfoFileScanner = require('./NfoFileScanner') const AbsMetadataFileScanner = require('./AbsMetadataFileScanner') +const EBookFile = require("../objects/files/EBookFile") /** * Metadata for books pulled from files @@ -84,7 +85,7 @@ class BookScanner { // Update audio files that were modified if (libraryItemData.audioLibraryFilesModified.length) { - let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified) + let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new)) media.audioFiles = media.audioFiles.map((audioFileObj) => { let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === audioFileObj.metadata.path) if (!matchedScannedAudioFile) { @@ -138,11 +139,25 @@ class BookScanner { } // Check if cover was removed - if (media.coverPath && !libraryItemData.imageLibraryFiles.some(lf => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) { + if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) { media.coverPath = null hasMediaChanges = true } + // Update cover if it was modified + if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) { + let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath) + if (coverMatch) { + const coverPath = coverMatch.new.metadata.path + if (coverPath !== media.coverPath) { + libraryScan.addLog(LogLevel.DEBUG, `Updating book cover "${media.coverPath}" => "${coverPath}" for book "${media.title}"`) + media.coverPath = coverPath + media.changed('coverPath', true) + hasMediaChanges = true + } + } + } + // Check if cover is not set and image files were found if (!media.coverPath && libraryItemData.imageLibraryFiles.length) { // Prefer using a cover image with the name "cover" otherwise use the first image @@ -157,6 +172,19 @@ class BookScanner { hasMediaChanges = true } + // Update ebook if it was modified + if (media.ebookFile && libraryItemData.ebookLibraryFilesModified.length) { + let ebookMatch = libraryItemData.ebookLibraryFilesModified.find(eFile => eFile.old.metadata.path === media.ebookFile.metadata.path) + if (ebookMatch) { + const ebookFile = new EBookFile(ebookMatch.new) + ebookFile.ebookFormat = ebookFile.metadata.ext.slice(1).toLowerCase() + libraryScan.addLog(LogLevel.DEBUG, `Updating book ebook file "${media.ebookFile.metadata.path}" => "${ebookFile.metadata.path}" for book "${media.title}"`) + media.ebookFile = ebookFile.toJSON() + media.changed('ebookFile', true) + hasMediaChanges = true + } + } + // Check if ebook is not set and ebooks were found if (!media.ebookFile && !librarySettings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) { // Prefer to use an epub ebook then fallback to the first ebook found diff --git a/server/scanner/LibraryItemScanData.js b/server/scanner/LibraryItemScanData.js index b604e4d7..d5a4a7a2 100644 --- a/server/scanner/LibraryItemScanData.js +++ b/server/scanner/LibraryItemScanData.js @@ -4,6 +4,12 @@ const LibraryItem = require('../models/LibraryItem') const globals = require('../utils/globals') class LibraryItemScanData { + /** + * @typedef LibraryFileModifiedObject + * @property {LibraryItem.LibraryFileObject} old + * @property {LibraryItem.LibraryFileObject} new + */ + constructor(data) { /** @type {string} */ this.libraryFolderId = data.libraryFolderId @@ -39,7 +45,7 @@ class LibraryItemScanData { this.libraryFilesRemoved = [] /** @type {LibraryItem.LibraryFileObject[]} */ this.libraryFilesAdded = [] - /** @type {LibraryItem.LibraryFileObject[]} */ + /** @type {LibraryFileModifiedObject[]} */ this.libraryFilesModified = [] } @@ -77,9 +83,9 @@ class LibraryItemScanData { return (this.audioLibraryFilesRemoved.length + this.audioLibraryFilesAdded.length + this.audioLibraryFilesModified.length) > 0 } - /** @type {LibraryItem.LibraryFileObject[]} */ + /** @type {LibraryFileModifiedObject[]} */ get audioLibraryFilesModified() { - return this.libraryFilesModified.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesModified.filter(lf => globals.SupportedAudioTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ @@ -97,12 +103,42 @@ class LibraryItemScanData { return this.libraryFiles.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } + /** @type {LibraryFileModifiedObject[]} */ + get imageLibraryFilesModified() { + return this.libraryFilesModified.filter(lf => globals.SupportedImageTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get imageLibraryFilesRemoved() { + return this.libraryFilesRemoved.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get imageLibraryFilesAdded() { + return this.libraryFilesAdded.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + /** @type {LibraryItem.LibraryFileObject[]} */ get imageLibraryFiles() { return this.libraryFiles.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } - /** @type {import('../objects/files/LibraryFile')[]} */ + /** @type {LibraryFileModifiedObject[]} */ + get ebookLibraryFilesModified() { + return this.libraryFilesModified.filter(lf => globals.SupportedEbookTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get ebookLibraryFilesRemoved() { + return this.libraryFilesRemoved.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get ebookLibraryFilesAdded() { + return this.libraryFilesAdded.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ get ebookLibraryFiles() { return this.libraryFiles.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } @@ -153,7 +189,7 @@ class LibraryItemScanData { existingLibraryItem[key] = this[key] this.hasChanges = true - if (key === 'relPath') { + if (key === 'relPath' || key === 'path') { this.hasPathChange = true } } @@ -202,8 +238,9 @@ class LibraryItemScanData { this.hasChanges = true } else { libraryFilesAdded = libraryFilesAdded.filter(lf => lf !== matchingLibraryFile) + let existingLibraryFileBefore = structuredClone(existingLibraryFile) if (this.compareUpdateLibraryFile(existingLibraryItem.path, existingLibraryFile, matchingLibraryFile, libraryScan)) { - this.libraryFilesModified.push(existingLibraryFile) + this.libraryFilesModified.push({old: existingLibraryFileBefore, new: existingLibraryFile}) this.hasChanges = true } } diff --git a/server/scanner/PodcastScanner.js b/server/scanner/PodcastScanner.js index 07dcbb11..4958d5f7 100644 --- a/server/scanner/PodcastScanner.js +++ b/server/scanner/PodcastScanner.js @@ -71,7 +71,7 @@ class PodcastScanner { // Update audio files that were modified if (libraryItemData.audioLibraryFilesModified.length) { - let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified) + let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new)) for (const podcastEpisode of existingPodcastEpisodes) { let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === podcastEpisode.audioFile.metadata.path) @@ -132,11 +132,25 @@ class PodcastScanner { let hasMediaChanges = false // Check if cover was removed - if (media.coverPath && !libraryItemData.imageLibraryFiles.some(lf => lf.metadata.path === media.coverPath)) { + if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath)) { media.coverPath = null hasMediaChanges = true } + // Update cover if it was modified + if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) { + let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath) + if (coverMatch) { + const coverPath = coverMatch.new.metadata.path + if (coverPath !== media.coverPath) { + libraryScan.addLog(LogLevel.DEBUG, `Updating podcast cover "${media.coverPath}" => "${coverPath}" for podcast "${media.title}"`) + media.coverPath = coverPath + media.changed('coverPath', true) + hasMediaChanges = true + } + } + } + // Check if cover is not set and image files were found if (!media.coverPath && libraryItemData.imageLibraryFiles.length) { // Prefer using a cover image with the name "cover" otherwise use the first image From f661e0835ce3653640dabcc19559348c0c70dff2 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Tue, 19 Mar 2024 19:18:38 +0100 Subject: [PATCH 0465/2145] Auth: Simplify Code --- server/Auth.js | 277 ++++++++++++++++++++++++++----------------------- 1 file changed, 147 insertions(+), 130 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index a4cdd1fc..368f9a4d 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -98,139 +98,156 @@ class Auth { scope: 'openid profile email' } }, async (tokenset, userinfo, done) => { - Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2)) + try { + Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2)) + + if (!userinfo.sub) { + throw new Error('Invalid userinfo, no sub') + } + + if (!this.validateGroupClaim(userinfo)) { + throw new Error(`Group claim ${Database.serverSettings.authOpenIDGroupClaim} not found or empty in userinfo`) + } + + let user = await this.findOrCreateUser(userinfo) + + if (!user || !user.isActive) { + throw new Error('User not active or not found') + } + + await this.setUserGroup(user, userinfo) + await this.updateUserPermissions(user, userinfo) + + // We also have to save the id_token for later (used for logout) because we cannot set cookies here + user.openid_id_token = tokenset.id_token - let failureMessage = 'Unauthorized' - if (!userinfo.sub) { - Logger.error(`[Auth] openid callback invalid userinfo, no sub`) - return done(null, null, failureMessage) + return done(null, user) + } catch (error) { + Logger.error(`[Auth] openid callback error: ${error?.message}\n${error?.stack}`) + + return done(null, null, 'Unauthorized') } - - // Check if the claims itself are returned correctly - const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; - if (groupClaimName) { - if (!userinfo[groupClaimName]) { - Logger.error(`[Auth] openid callback invalid: Group claim ${groupClaimName} configured, but not found or empty in userinfo`) - return done(null, null, failureMessage) - } - - const groupsList = userinfo[groupClaimName] - const targetRoles = ['admin', 'user', 'guest'] - - // Convert the list to lowercase for case-insensitive comparison - const groupsListLowercase = groupsList.map(group => group.toLowerCase()) - - // Check if any of the target roles exist in the groups list - const containsTargetRole = targetRoles.some(role => groupsListLowercase.includes(role.toLowerCase())) - - if (!containsTargetRole) { - Logger.info(`[Auth] openid callback: Denying access because neither admin nor user or guest is included is inside the group claim. Groups found: `, groupsList) - return done(null, null, failureMessage) - } - } - - const advancedPermsClaimName = Database.serverSettings.authOpenIDAdvancedPermsClaim - if (advancedPermsClaimName && !userinfo[advancedPermsClaimName]) { - Logger.error(`[Auth] openid callback invalid: Advanced perms claim ${advancedPermsClaimName} configured, but not found or empty in userinfo`) - 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 - } - - // Set user group if name of groups claim is configured - if (groupClaimName) { - const groupsList = userinfo[groupClaimName] ? userinfo[groupClaimName].map(group => group.toLowerCase()) : [] - const rolesInOrderOfPriority = ['admin', 'user', 'guest'] - - let userType = null - - for (let role of rolesInOrderOfPriority) { - if (groupsList.includes(role)) { - userType = role // This will override with the highest priority role found - break // Stop searching once the highest priority role is found - } - } - - // Actually already checked above, but just to be sure - if (!userType) { - Logger.error(`[Auth] openid callback: Denying access because neither admin nor user or guest is included is inside the group claim. Groups found: `, groupsList) - return done(null, null, failureMessage) - } - - Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`) - user.type = userType - await Database.userModel.updateFromOld(user) - } - - if (advancedPermsClaimName) { - try { - Logger.debug(`[Auth] openid callback: Updating advanced perms for user ${user.username} to ${JSON.stringify(userinfo[advancedPermsClaimName])}`) - - user.updatePermissionsFromExternalJSON(userinfo[advancedPermsClaimName]) - await Database.userModel.updateFromOld(user) - } catch (error) { - Logger.error(`[Auth] openid callback: Error updating advanced perms for user, error: `, error) - return done(null, null, failureMessage) - } - } - - // We also have to save the id_token for later (used for logout) because we cannot set cookies here - user.openid_id_token = tokenset.id_token - - // permit login - return done(null, user) })) } + /** + * Finds an existing user by OpenID subject identifier, or by email/username based on server settings, + * or creates a new user if configured to do so. + */ + async findOrCreateUser(userinfo) { + let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) + + // Matched by sub + if (user) { + Logger.debug(`[Auth] openid: User found by sub`) + return user + } + + // Match existing user by email + 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) + + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) + return null // User is linked to a different OpenID subject; do not proceed. + } + } + // Match existing user by username + 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) + + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) + return null // User is linked to a different OpenID subject; do not proceed. + } + } + + // Found existing user via email or username + if (user) { + if (!user.isActive) { + Logger.warn(`[Auth] openid: User found but is not active`) + return null + } + + user.authOpenIDSub = userinfo.sub + await Database.userModel.updateFromOld(user) + + Logger.debug(`[Auth] openid: User found by email/username`) + return user + } + + // If no existing user was matched, auto-register if configured + if (Database.serverSettings.authOpenIDAutoRegister) { + Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo) + user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this) + return user + } + + Logger.warn(`[Auth] openid: User not found and auto-register is disabled`) + return null + } + + /** + * Validates the presence and content of the group claim in userinfo. + */ + validateGroupClaim(userinfo) { + const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; + if (!groupClaimName) // Allow no group claim when configured like this + return true + + // If configured it must exist in userinfo + if (!userinfo[groupClaimName]) { + return false + } + return true + } + +/** + * Sets the user group based on group claim in userinfo. + */ +async setUserGroup(user, userinfo) { + const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; + if (!groupClaimName) // No group claim configured, don't set anything + return + + if (!userinfo[groupClaimName]) + throw new Error(`Group claim ${groupClaimName} not found in userinfo`) + + const groupsList = userinfo[groupClaimName].map(group => group.toLowerCase()) + const rolesInOrderOfPriority = ['admin', 'user', 'guest'] + + let userType = rolesInOrderOfPriority.find(role => groupsList.includes(role)) + if (userType) { + Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`) + + if (user.type !== userType) { + user.type = userType; + await Database.userModel.updateFromOld(user) + } + } else { + throw new Error(`No valid group found in userinfo: ${JSON.stringify(userinfo[groupClaimName], null, 2)}`) + } +} + +/** + * Updates user permissions based on the advanced permissions claim. + */ +async updateUserPermissions(user, userinfo) { + const absPermissionsClaim = Database.serverSettings.authOpenIDAdvancedPermsClaim + if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything + return + + const absPermissions = userinfo[absPermissionsClaim] + if (!absPermissions) + throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`) + + Logger.debug(`[Auth] openid callback: Updating advanced perms for user ${user.username} to ${JSON.stringify(absPermissions)}`) + user.updatePermissionsFromExternalJSON(absPermissions) + await Database.userModel.updateFromOld(user) +} + /** * Unuse strategy * @@ -421,7 +438,7 @@ class Auth { res.redirect(authorizationUrl) } catch (error) { - Logger.error(`[Auth] Error in /auth/openid route: ${error}`) + Logger.error(`[Auth] Error in /auth/openid route: ${error}\n${error?.stack}`) res.status(500).send('Internal Server Error') } @@ -477,7 +494,7 @@ class Auth { // 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}`) + Logger.error(`[Auth] Error in /auth/openid/mobile-redirect route: ${error}\n${error?.stack}`) res.status(500).send('Internal Server Error') } }) From 50330b0a606901f320cdb8eda802575db2aa3ae6 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Tue, 19 Mar 2024 19:18:47 +0100 Subject: [PATCH 0466/2145] Auth: Add translations --- client/pages/config/authentication.vue | 11 +++-------- client/strings/de.json | 3 +++ client/strings/en-us.json | 3 +++ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 91c6cfe2..cecccee4 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -82,15 +82,12 @@ <p class="pl-4 text-sm text-gray-300">{{ $strings.LabelAutoRegisterDescription }}</p> </div> - <div class="flex items-center pt-6 pb-1 px-1 w-full">Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.</div> + <div class="flex items-center pt-6 pb-1 px-1 w-full">{{ $strings.LabelOpenIDClaims }}</div> <div class="flex items-center mb-2"> <div class="w-96"> <ui-text-input-with-label ref="openidGroupClaim" v-model="newAuthSettings.authOpenIDGroupClaim" :disabled="savingSettings" :placeholder="'groups'" :label="'Group Claim'" /> </div> - <p class="pl-4 text-sm text-gray-300 mt-5"> - Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to - multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied. - </p> + <p class="pl-4 text-sm text-gray-300 mt-5" v-html="$strings.LabelOpenIDGroupClaimDescription"></p> </div> <div class="flex mb-2"> @@ -98,9 +95,7 @@ <ui-text-input-with-label ref="openidAdvancedPermsClaim" v-model="newAuthSettings.authOpenIDAdvancedPermsClaim" :disabled="savingSettings" :placeholder="'abspermissions'" :label="'Advanced Permission Claim'" /> </div> <div class="pl-4 text-sm text-gray-300 mt-5 flex-column"> - <p class=""> - Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure: - </p> + <p v-html="$strings.LabelOpenIDAdvancedPermsClaimDescription"></p> <pre class="text-pre-wrap mt-2" >{{ newAuthSettings.authOpenIDSamplePermissions }} </pre> diff --git a/client/strings/de.json b/client/strings/de.json index ed99f095..611432f1 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Nicht begonnen", "LabelNumberOfBooks": "Anzahl der Hörbücher", "LabelNumberOfEpisodes": "Anzahl der Episoden", + "LabelOpenIDClaims": "Lass die folgenden Optionen leer, um die erweiterte Zuweisung von Gruppen und Berechtigungen zu deaktivieren und automatisch die 'User'-Gruppe zuzuweisen.", + "LabelOpenIDGroupClaimDescription": "Name des OpenID-Claims, der eine Liste der Benutzergruppen enthält. Wird häufig als <code>groups</code> bezeichnet. <b>Wenn konfiguriert</b>, wird die Anwendung automatisch Rollen basierend auf den Gruppenmitgliedschaften des Benutzers zuweisen, vorausgesetzt, dass diese Gruppen im Claim als 'admin', 'user' oder 'guest' benannt sind (Groß/Kleinschreibung ist irrelevant). Der Claim eine Liste sein, und wenn ein Benutzer mehreren Gruppen angehört, wird die Anwendung die Rolle zuordnen, die dem höchsten Zugriffslevel entspricht. Wenn keine Gruppe übereinstimmt, wird der Zugang verweigert.", + "LabelOpenIDAdvancedPermsClaimDescription": "Name des OpenID-Claims, der erweiterte Berechtigungen für Benutzeraktionen innerhalb der Anwendung enthält, die auf Nicht-Admin-Rollen angewendet werden (<b>wenn konfiguriert</b>). Wenn der Claim in der Antwort fehlt, wird der Zugang zu ABS verweigert. Fehlt eine einzelne Option, wird sie als <code>false</code> behandelt. Stelle sicher, dass der Claim des Identitätsanbieters der erwarteten Struktur entspricht:", "LabelOpenRSSFeed": "Öffne RSS-Feed", "LabelOverwrite": "Überschreiben", "LabelPassword": "Passwort", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 43a1ef44..b6fe3505 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Not Started", "LabelNumberOfBooks": "Number of Books", "LabelNumberOfEpisodes": "# of Episodes", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", "LabelOpenRSSFeed": "Open RSS Feed", "LabelOverwrite": "Overwrite", "LabelPassword": "Password", From 1646f0ebc21505a1ed00866cb7a033c5028ba5c4 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Tue, 19 Mar 2024 19:35:34 +0100 Subject: [PATCH 0467/2145] OpenID: Ignore admin for advanced permissions Also removed some semicolons --- server/Auth.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 368f9a4d..e14348c7 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -193,7 +193,7 @@ class Auth { * Validates the presence and content of the group claim in userinfo. */ validateGroupClaim(userinfo) { - const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; + const groupClaimName = Database.serverSettings.authOpenIDGroupClaim if (!groupClaimName) // Allow no group claim when configured like this return true @@ -208,7 +208,7 @@ class Auth { * Sets the user group based on group claim in userinfo. */ async setUserGroup(user, userinfo) { - const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; + const groupClaimName = Database.serverSettings.authOpenIDGroupClaim if (!groupClaimName) // No group claim configured, don't set anything return @@ -223,7 +223,7 @@ async setUserGroup(user, userinfo) { Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`) if (user.type !== userType) { - user.type = userType; + user.type = userType await Database.userModel.updateFromOld(user) } } else { @@ -239,6 +239,9 @@ async updateUserPermissions(user, userinfo) { if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything return + if (user.type === 'admin') + return + const absPermissions = userinfo[absPermissionsClaim] if (!absPermissions) throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`) From 1d7434cbbbbe3d3f1349d266bc717d9c11e07367 Mon Sep 17 00:00:00 2001 From: Illia Pyshniak <corbee@pm.me> Date: Tue, 19 Mar 2024 23:12:29 +0200 Subject: [PATCH 0468/2145] Create uk.json --- client/strings/uk.json | 781 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 781 insertions(+) create mode 100644 client/strings/uk.json diff --git a/client/strings/uk.json b/client/strings/uk.json new file mode 100644 index 00000000..9953bda5 --- /dev/null +++ b/client/strings/uk.json @@ -0,0 +1,781 @@ +{ + "ButtonAdd": "Додати", + "ButtonAddChapters": "Додати глави", + "ButtonAddDevice": "Додати пристрій", + "ButtonAddLibrary": "Додати бібліотеку", + "ButtonAddPodcasts": "Додати подкаст", + "ButtonAddUser": "Додати користувача", + "ButtonAddYourFirstLibrary": "Додайте вашу першу бібліотеку", + "ButtonApply": "Застосувати", + "ButtonApplyChapters": "Зберегти глави", + "ButtonAuthors": "Автори", + "ButtonBrowseForFolder": "Огляд тек", + "ButtonCancel": "Скасувати", + "ButtonCancelEncode": "Скасувати кодування", + "ButtonChangeRootPassword": "Змінити кореневий пароль", + "ButtonCheckAndDownloadNewEpisodes": "Перевірити та завантажити нові епізоди", + "ButtonChooseAFolder": "Обрати теку", + "ButtonChooseFiles": "Обрати файли", + "ButtonClearFilter": "Очистити фільтр", + "ButtonCloseFeed": "Закрити стрічку", + "ButtonCollections": "Колекції", + "ButtonConfigureScanner": "Налаштувати сканер", + "ButtonCreate": "Створити", + "ButtonCreateBackup": "Створити резервну копію", + "ButtonDelete": "Видалити", + "ButtonDownloadQueue": "Черга", + "ButtonEdit": "Редагувати", + "ButtonEditChapters": "Редагувати глави", + "ButtonEditPodcast": "Редагувати подкаст", + "ButtonForceReScan": "Примусово сканувати", + "ButtonFullPath": "Повний шлях", + "ButtonHide": "Приховати", + "ButtonHome": "Головна", + "ButtonIssues": "Проблеми", + "ButtonJumpBackward": "Перейти назад", + "ButtonJumpForward": "Перейти вперед", + "ButtonLatest": "Останній", + "ButtonLibrary": "Бібліотека", + "ButtonLogout": "Вийти", + "ButtonLookup": "Пошук", + "ButtonManageTracks": "Керувати доріжками", + "ButtonMapChapterTitles": "Призначити назви глав", + "ButtonMatchAllAuthors": "Віднайти усіх авторів", + "ButtonMatchBooks": "Віднайти книги", + "ButtonNevermind": "Скасувати", + "ButtonNext": "Наступний", + "ButtonNextChapter": "Наступна глава", + "ButtonOk": "Гаразд", + "ButtonOpenFeed": "Відкрити стрічку", + "ButtonOpenManager": "Відкрити менеджер", + "ButtonPause": "Пауза", + "ButtonPlay": "Грати", + "ButtonPlaying": "Відтворюється", + "ButtonPlaylists": "Списки відтворення", + "ButtonPrevious": "Попередній", + "ButtonPreviousChapter": "Попередня глава", + "ButtonPurgeAllCache": "Очистити весь кеш", + "ButtonPurgeItemsCache": "Очистити кеш елементів", + "ButtonPurgeMediaProgress": "Очистити прогрес", + "ButtonQueueAddItem": "Додати до черги", + "ButtonQueueRemoveItem": "Вилучити з черги", + "ButtonQuickMatch": "Швидкий пошук", + "ButtonRead": "Читати", + "ButtonRefresh": "Оновити", + "ButtonRemove": "Видалити", + "ButtonRemoveAll": "Видалити все", + "ButtonRemoveAllLibraryItems": "Видалити всі елементи бібліотеки", + "ButtonRemoveFromContinueListening": "Видалити з Продовжити слухати", + "ButtonRemoveFromContinueReading": "Видалити з Продовжити читання", + "ButtonRemoveSeriesFromContinueSeries": "Видалити серію з Продовжити серії", + "ButtonReScan": "Пересканувати", + "ButtonReset": "Скинути", + "ButtonResetToDefault": "Скинути до стандартних", + "ButtonRestore": "Відновити", + "ButtonSave": "Зберегти", + "ButtonSaveAndClose": "Зберегти та закрити", + "ButtonSaveTracklist": "Зберегти порядок", + "ButtonScan": "Сканувати", + "ButtonScanLibrary": "Сканувати бібліотеку", + "ButtonSearch": "Пошук", + "ButtonSelectFolderPath": "Обрати шлях до теки", + "ButtonSeries": "Серії", + "ButtonSetChaptersFromTracks": "Встановити глави за доріжками", + "ButtonShare": "Поширити", + "ButtonShiftTimes": "Зсунути час", + "ButtonShow": "Показати", + "ButtonStartM4BEncode": "Почати кодування у M4B", + "ButtonStartMetadataEmbed": "Почати вбудування метаданих", + "ButtonSubmit": "Надіслати", + "ButtonTest": "Перевірити", + "ButtonUpload": "Завантажити", + "ButtonUploadBackup": "Завантажити резервну копію", + "ButtonUploadCover": "Завантажити обкладинку", + "ButtonUploadOPMLFile": "Завантажити OPML-файл", + "ButtonUserDelete": "Видалити користувача {0}", + "ButtonUserEdit": "Редагувати користувача {0}", + "ButtonViewAll": "Переглянути все", + "ButtonYes": "Так", + "ErrorUploadFetchMetadataAPI": "Помилка при отриманні метаданих", + "ErrorUploadFetchMetadataNoResults": "Не вдалося отримати метадані — спробуйте оновити заголовок та/або автора", + "ErrorUploadLacksTitle": "Назва обов'язкова", + "HeaderAccount": "Профіль", + "HeaderAdvanced": "Розширені", + "HeaderAppriseNotificationSettings": "Налаштування сповіщень Apprise", + "HeaderAudiobookTools": "Інструменти керування файлами книг", + "HeaderAudioTracks": "Аудіодоріжки", + "HeaderAuthentication": "Автентифікація", + "HeaderBackups": "Резервні копії", + "HeaderChangePassword": "Змінити пароль", + "HeaderChapters": "Глави", + "HeaderChooseAFolder": "Обрати теку", + "HeaderCollection": "Колекція", + "HeaderCollectionItems": "Елементи колекції", + "HeaderCover": "Обкладинка", + "HeaderCurrentDownloads": "Поточні завантаження", + "HeaderCustomMetadataProviders": "Постачальники метаданих", + "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": "Сповіщення", + "HeaderOpenIDConnectAuthentication": "Автентифікація OpenID Connect", + "HeaderOpenRSSFeed": "Відкрити RSS-канал", + "HeaderOtherFiles": "Інші файли", + "HeaderPasswordAuthentication": "Автентифікація за паролем", + "HeaderPermissions": "Дозволи", + "HeaderPlayerQueue": "Черга відтворення", + "HeaderPlaylist": "Список відтворення", + "HeaderPlaylistItems": "Елементи списку відтворення", + "HeaderPodcastsToAdd": "Додати подкасти", + "HeaderPreviewCover": "Попередній перегляд", + "HeaderRemoveEpisode": "Видалити епізод", + "HeaderRemoveEpisodes": "Видалити епізодів: {0}", + "HeaderRSSFeedGeneral": "Подробиці RSS", + "HeaderRSSFeedIsOpen": "RSS-канал відкрито", + "HeaderRSSFeeds": "RSS-канали", + "HeaderSavedMediaProgress": "Збережений прогрес медіа", + "HeaderSchedule": "Розклад", + "HeaderScheduleLibraryScans": "Розклад автосканування бібліотеки", + "HeaderSession": "Сеанс", + "HeaderSetBackupSchedule": "Встановити розклад резервного копіювання", + "HeaderSettings": "Налаштування", + "HeaderSettingsDisplay": "Відображення", + "HeaderSettingsExperimental": "Експериментальні функції", + "HeaderSettingsGeneral": "Основне", + "HeaderSettingsScanner": "Сканер", + "HeaderSleepTimer": "Таймер вимкнення", + "HeaderStatsLargestItems": "Найбільші елементи", + "HeaderStatsLongestItems": "Найдовші елементи (год)", + "HeaderStatsMinutesListeningChart": "Хвилин прослухано (останні 7 днів)", + "HeaderStatsRecentSessions": "Останні сеанси", + "HeaderStatsTop10Authors": "10 улюблених авторів", + "HeaderStatsTop5Genres": "5 улюблених жанрів", + "HeaderTableOfContents": "Зміст", + "HeaderTools": "Інструменти", + "HeaderUpdateAccount": "Оновити профіль", + "HeaderUpdateAuthor": "Оновити автора", + "HeaderUpdateDetails": "Оновити подробиці", + "HeaderUpdateLibrary": "Оновити бібліотеку", + "HeaderUsers": "Користувачі", + "HeaderYearReview": "Підсумки {0} року", + "HeaderYourStats": "Ваша статистика", + "LabelAbridged": "Скорочена", + "LabelAccountType": "Тип профілю", + "LabelAccountTypeAdmin": "Адміністратор", + "LabelAccountTypeGuest": "Гість", + "LabelAccountTypeUser": "Користувач", + "LabelActivity": "Активність", + "LabelAdded": "Додано", + "LabelAddedAt": "Дата додавання", + "LabelAddToCollection": "Додати у добірку", + "LabelAddToCollectionBatch": "Додати книги до добірки: {0}", + "LabelAddToPlaylist": "Додати до списку відтворення", + "LabelAddToPlaylistBatch": "Додано елементів у список відтворення: {0}", + "LabelAdminUsersOnly": "Тільки для адміністраторів", + "LabelAll": "Усе", + "LabelAllUsers": "Усі користувачі", + "LabelAllUsersExcludingGuests": "Усі, крім гостей", + "LabelAllUsersIncludingGuests": "Усі, включно з гостями", + "LabelAlreadyInYourLibrary": "Вже у вашій бібліотеці", + "LabelAppend": "Додати", + "LabelAuthor": "Автор", + "LabelAuthorFirstLast": "Автор (за ім'ям)", + "LabelAuthorLastFirst": "Автор (за прізвищем)", + "LabelAuthors": "Автори", + "LabelAutoDownloadEpisodes": "Автозавантаження епізодів", + "LabelAutoFetchMetadata": "Автозавантаження метаданих", + "LabelAutoFetchMetadataHelp": "Отримує метадані про назву, автора та серію під час послідового завантаження. Після завантаження може знадобитися пошук додаткових метаданих.", + "LabelAutoLaunch": "Автозапуск", + "LabelAutoLaunchDescription": "Автоматично перенаправляти зі сторінки входу до сервісу автентифікації (ручний перезапис шляху <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "Автореєстрація", + "LabelAutoRegisterDescription": "Автоматично створювати нових користувачів після входу", + "LabelBackToUser": "Повернутися до користувача", + "LabelBackupLocation": "Розташування резервних копій", + "LabelBackupsEnableAutomaticBackups": "Автоматичне резервне копіювання", + "LabelBackupsEnableAutomaticBackupsHelp": "Резервні копії збережено у /metadata/backups", + "LabelBackupsMaxBackupSize": "Максимальний розмір резервної копії (у ГБ)", + "LabelBackupsMaxBackupSizeHelp": "У якості захисту від неправильного налаштування, резервну копію не буде збережено, якщо її розмір перевищуватиме вказаний.", + "LabelBackupsNumberToKeep": "Кількість резервних копій", + "LabelBackupsNumberToKeepHelp": "Лиш 1 резервну копію буде видалено за раз, тож якщо їх багато, то вам варто видалити їх вручну.", + "LabelBitrate": "Бітрейт", + "LabelBooks": "Книги", + "LabelButtonText": "Текст кнопки", + "LabelChangePassword": "Змінити пароль", + "LabelChannels": "Канали", + "LabelChapters": "Глави", + "LabelChaptersFound": "глав знайдено", + "LabelChapterTitle": "Назва глави", + "LabelClickForMoreInfo": "Натисніть, щоб дізнатися більше", + "LabelClosePlayer": "Закрити програвач", + "LabelCodec": "Кодек", + "LabelCollapseSeries": "Згорнути серії", + "LabelCollection": "Добірка", + "LabelCollections": "Добірки", + "LabelComplete": "Завершити", + "LabelConfirmPassword": "Підтвердити пароль", + "LabelContinueListening": "Слухати далі", + "LabelContinueReading": "Читати далі", + "LabelContinueSeries": "Продовжити серії", + "LabelCover": "Обкладинка", + "LabelCoverImageURL": "URL-адреса обкладинки", + "LabelCreatedAt": "Дата створення", + "LabelCronExpression": "Команда cron", + "LabelCurrent": "Поточне", + "LabelCurrently": "Поточний:", + "LabelCustomCronExpression": "Спецільна команда cron:", + "LabelDatetime": "Дата й час", + "LabelDeleteFromFileSystemCheckbox": "Видалити з файлової системи (зніміть прапорець, щоб видалити лише з бази даних)", + "LabelDescription": "Опис", + "LabelDeselectAll": "Скасувати вибір", + "LabelDevice": "Пристрій", + "LabelDeviceInfo": "Про пристрій", + "LabelDeviceIsAvailableTo": "Пристрій доступний для...", + "LabelDirectory": "Каталог", + "LabelDiscFromFilename": "Диск за назвою файлу", + "LabelDiscFromMetadata": "Диск за метаданими", + "LabelDiscover": "Огляд", + "LabelDownload": "Завантажити", + "LabelDownloadNEpisodes": "Завантажити епізодів: {0}", + "LabelDuration": "Тривалість", + "LabelDurationFound": "Виявлена тривалість:", + "LabelEbook": "Електронна книга", + "LabelEbooks": "Електронні книги", + "LabelEdit": "Редагувати", + "LabelEmail": "Електронна пошта", + "LabelEmailSettingsFromAddress": "Адреса відправника", + "LabelEmailSettingsSecure": "Безпечне", + "LabelEmailSettingsSecureHelp": "Увімкніть, аби використовувати TLS при підключенні до сервера. Якщо вимкнути, то TLS буде використано, якщо сервер підтримує STARTTLS. Увімкніть, якщо ви підключаєтеся до порту 465. Вимкніть для портів 587 або 25. (з nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Тестова адреса", + "LabelEmbeddedCover": "Вбудована обкладинка", + "LabelEnable": "Увімкнути", + "LabelEnd": "Кінець", + "LabelEpisode": "Епізод", + "LabelEpisodeTitle": "Назва епізоду", + "LabelEpisodeType": "Тип епізоду", + "LabelExample": "Приклад", + "LabelExplicit": "Відверта", + "LabelFeedURL": "Адреса стрічки", + "LabelFetchingMetadata": "Отримання метаданих", + "LabelFile": "Файл", + "LabelFileBirthtime": "Дата створення", + "LabelFileModified": "Дата змінення", + "LabelFilename": "Ім'я файлу", + "LabelFilterByUser": "Фільтрувати за користувачем", + "LabelFindEpisodes": "Знайти епізоди", + "LabelFinished": "Завершено", + "LabelFolder": "Тека", + "LabelFolders": "Теки", + "LabelFontBold": "Жирний", + "LabelFontFamily": "Гарнітура", + "LabelFontItalic": "Курсив", + "LabelFontScale": "Розмір шрифту", + "LabelFontStrikethrough": "Закреслений", + "LabelFormat": "Формат", + "LabelGenre": "Жанр", + "LabelGenres": "Жанри", + "LabelHardDeleteFile": "Остаточно видалити файл", + "LabelHasEbook": "Має електронну книгу", + "LabelHasSupplementaryEbook": "Має додаткову електронну книгу", + "LabelHighestPriority": "Найвищий пріоритет", + "LabelHost": "Гост", + "LabelHour": "Година", + "LabelIcon": "Іконка", + "LabelImageURLFromTheWeb": "URL зображення з мережі", + "LabelIncludeInTracklist": "Включити у список", + "LabelIncomplete": "Не завершено", + "LabelInProgress": "У процесі", + "LabelInterval": "Частота", + "LabelIntervalCustomDailyWeekly": "Налаштувати щодня/щотижня", + "LabelIntervalEvery12Hours": "Кожні 12 годин", + "LabelIntervalEvery15Minutes": "Кожні 15 хвилин", + "LabelIntervalEvery2Hours": "Кожні 2 години", + "LabelIntervalEvery30Minutes": "Кожні 30 хвилин", + "LabelIntervalEvery6Hours": "Кожні 6 годин", + "LabelIntervalEveryDay": "Щодня", + "LabelIntervalEveryHour": "Щогодини", + "LabelInvalidParts": "Недопустимі частини", + "LabelInvert": "Інвертувати", + "LabelItem": "Елемент", + "LabelLanguage": "Мова", + "LabelLanguageDefaultServer": "Типова мова сервера", + "LabelLastBookAdded": "Останню книгу додано", + "LabelLastBookUpdated": "Останню книгу оновлено", + "LabelLastSeen": "Активність", + "LabelLastTime": "Останній час", + "LabelLastUpdate": "Останнє оновлення", + "LabelLayout": "Вигляд", + "LabelLayoutSinglePage": "Одна сторінка", + "LabelLayoutSplitPage": "Розділити сторінку", + "LabelLess": "Менше", + "LabelLibrariesAccessibleToUser": "Бібліотеки, доступні користувачу", + "LabelLibrary": "Бібліотека", + "LabelLibraryItem": "Елемент бібліотеки", + "LabelLibraryName": "Назва бібліотеки", + "LabelLimit": "Обмеження", + "LabelLineSpacing": "Відстань між рядками", + "LabelListenAgain": "Слухати знову", + "LabelLogLevelDebug": "Зневадження", + "LabelLogLevelInfo": "Відомості", + "LabelLogLevelWarn": "Увага", + "LabelLookForNewEpisodesAfterDate": "Шукати нові епізоди після вказаної дати", + "LabelLowestPriority": "Найнижчий пріоритет", + "LabelMatchExistingUsersBy": "Шукати наявних користувачів за", + "LabelMatchExistingUsersByDescription": "Використовується для підключення наявних користувачів. Після підключення користувач отримає унікальний id від вашого сервісу SSO", + "LabelMediaPlayer": "Програвач медіа", + "LabelMediaType": "Тип медіа", + "LabelMetadataOrderOfPrecedenceDescription": "Пріоритетніші джерела метаданих перезапишуть менш пріоритетні метадані", + "LabelMetadataProvider": "Джерело метаданих", + "LabelMetaTag": "Метатег", + "LabelMetaTags": "Метатеги", + "LabelMinute": "Хвилина", + "LabelMissing": "Бракує", + "LabelMissingEbook": "Без електронної книги", + "LabelMissingParts": "Відсутні частини", + "LabelMissingSupplementaryEbook": "Без додаткової електронної книги", + "LabelMobileRedirectURIs": "Дозволені адреси перенаправлення", + "LabelMobileRedirectURIsDescription": "Це білий список наявних URI, що перенаправляють у мобільний додаток. За замовчуванням це <code>audiobookshelf://oauth</code>, який ви можете видалити або ж додати інші адреси для сторонніх інтеграцій. Використайте зірочку (<code>*</code>), аби дозволити будь-яке URI.", + "LabelMore": "Більше", + "LabelMoreInfo": "Докладніше", + "LabelName": "Назва", + "LabelNarrator": "Читець", + "LabelNarrators": "Читці", + "LabelNew": "Нове", + "LabelNewestAuthors": "Нові автори", + "LabelNewestEpisodes": "Нові епізоди", + "LabelNewPassword": "Новий пароль", + "LabelNextBackupDate": "Дата наступного резервного копіювання", + "LabelNextScheduledRun": "Наступний запланований запуск", + "LabelNoEpisodesSelected": "Не вибрано жодного епізоду", + "LabelNotes": "Примітки", + "LabelNotFinished": "Незавершені", + "LabelNotificationAppriseURL": "URL Apprise", + "LabelNotificationAvailableVariables": "Доступні змінні", + "LabelNotificationBodyTemplate": "Шаблон сповіщення", + "LabelNotificationEvent": "Сповіщення про події", + "LabelNotificationsMaxFailedAttempts": "Ліміт невдалих спроб", + "LabelNotificationsMaxFailedAttemptsHelp": "Сповіщення буде вимкнено після багатьох невдалих надсилань", + "LabelNotificationsMaxQueueSize": "Ліміт розміру черги сповіщень", + "LabelNotificationsMaxQueueSizeHelp": "Події обмежені до 1 на секунду. Події буде проігноровано, якщо ліміт черги досягнуто. Це запобігає спаму сповіщеннями.", + "LabelNotificationTitleTemplate": "Шаблон заголовку", + "LabelNotStarted": "Не розпочато", + "LabelNumberOfBooks": "Кількість книг", + "LabelNumberOfEpisodes": "Кількість епізодів", + "LabelOpenRSSFeed": "Відкрити RSS-канал", + "LabelOverwrite": "Перезаписати", + "LabelPassword": "Пароль", + "LabelPath": "Шлях", + "LabelPermissionsAccessAllLibraries": "Доступ до усіх бібліотек", + "LabelPermissionsAccessAllTags": "Доступ до усіх міток", + "LabelPermissionsAccessExplicitContent": "Доступ до відвертого вмісту", + "LabelPermissionsDelete": "Може видаляти", + "LabelPermissionsDownload": "Може завантажувати", + "LabelPermissionsUpdate": "Може оновлювати", + "LabelPermissionsUpload": "Може завантажувати", + "LabelPersonalYearReview": "Ваші підсумки року ({0})", + "LabelPhotoPathURL": "Шлях/URL фото", + "LabelPlaylists": "Списки відтворення", + "LabelPlayMethod": "Метод відтворення", + "LabelPodcast": "Подкаст", + "LabelPodcasts": "Подкасти", + "LabelPodcastSearchRegion": "Регіон пошуку подкасту", + "LabelPodcastType": "Тип подкасту", + "LabelPort": "Порт", + "LabelPrefixesToIgnore": "Ігнорувати префікси (з урахуванням регістру)", + "LabelPreventIndexing": "Заборонити індексування вашого каналу каталогами подкастів iTunes та Google", + "LabelPrimaryEbook": "Основна електронна книга", + "LabelProgress": "Прогрес", + "LabelProvider": "Джерело", + "LabelPubDate": "Дата публікації", + "LabelPublisher": "Видавець", + "LabelPublishYear": "Рік публікації", + "LabelRead": "Читати", + "LabelReadAgain": "Читати знову", + "LabelReadEbookWithoutProgress": "Читати книгу без збереження прогресу", + "LabelRecentlyAdded": "Нещодавно додані", + "LabelRecentSeries": "Останні серії", + "LabelRecommended": "Рекомендовані", + "LabelRedo": "Повторити", + "LabelRegion": "Регіон", + "LabelReleaseDate": "Дата публікації", + "LabelRemoveCover": "Видалити обкладинку", + "LabelRowsPerPage": "Рядків на сторінку", + "LabelRSSFeedCustomOwnerEmail": "Користувацька електронна адреса власника", + "LabelRSSFeedCustomOwnerName": "Користувацьке ім'я власника", + "LabelRSSFeedOpen": "RSS-канал відкрито", + "LabelRSSFeedPreventIndexing": "Запобігати індексації", + "LabelRSSFeedSlug": "Назва RSS-каналу", + "LabelRSSFeedURL": "Адреса RSS-каналу", + "LabelSearchTerm": "Пошуковий запит", + "LabelSearchTitle": "Пошук за назвою", + "LabelSearchTitleOrASIN": "Пошук назви або ASIN", + "LabelSeason": "Сезон", + "LabelSelectAllEpisodes": "Вибрати всі серії", + "LabelSelectEpisodesShowing": "Обрати показані епізоди: {0}", + "LabelSelectUsers": "Вибрати користувачів", + "LabelSendEbookToDevice": "Надіслати електронну книгу на...", + "LabelSequence": "Послідовність", + "LabelSeries": "Серії", + "LabelSeriesName": "Назва серії", + "LabelSeriesProgress": "Прогрес серії", + "LabelServerYearReview": "Підсумки року сервера ({0})", + "LabelSetEbookAsPrimary": "Зробити основною", + "LabelSetEbookAsSupplementary": "Зробити додатковою", + "LabelSettingsAudiobooksOnly": "Лише аудіокниги", + "LabelSettingsAudiobooksOnlyHelp": "Увімкніть цей параметр, щоб ігнорувати файли електронних книг, якщо вони не знаходяться у теці аудіокниги, тоді вони будуть встановлені як додаткові електронні книги", + "LabelSettingsBookshelfViewHelp": "Імітує вигляд дерев'яних полиць", + "LabelSettingsChromecastSupport": "Підтримка Chromecast", + "LabelSettingsDateFormat": "Формат дати", + "LabelSettingsDisableWatcher": "Вимкнути спостерігача", + "LabelSettingsDisableWatcherForLibrary": "Вимкнути спостерігання тек бібліотеки", + "LabelSettingsDisableWatcherHelp": "Вимикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера", + "LabelSettingsEnableWatcher": "Увімкнути спостерігача", + "LabelSettingsEnableWatcherForLibrary": "Увімкнути спостерігання тек бібліотеки", + "LabelSettingsEnableWatcherHelp": "Вмикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера", + "LabelSettingsExperimentalFeatures": "Експериментальні функції", + "LabelSettingsExperimentalFeaturesHelp": "Функції в розробці, які потребують вашого відгуку та допомоги в тестуванні. Натисніть, щоб відкрити обговорення на Github.", + "LabelSettingsFindCovers": "Пошук обкладинок", + "LabelSettingsFindCoversHelp": "Якщо ваша аудіокнига не містить вбудованої обкладинки або зображення у теці, сканувальник спробує знайти обкладинку.<br>Примітка: Це збільшить час сканування", + "LabelSettingsHideSingleBookSeries": "Сховати серії з однією книгою", + "LabelSettingsHideSingleBookSeriesHelp": "Серії, що містять одну книгу, будуть приховані зі сторінки серій та полиць головної сторінки.", + "LabelSettingsHomePageBookshelfView": "Полиці на головній сторінці", + "LabelSettingsLibraryBookshelfView": "Показувати полиці у бібліотеці", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропускати попередні книги у Продовжити серії", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Полиця Продовжити серії на головній сторінці показує найпершу непочату книгу з тих серій, у яких ви завершили хоча б одну книгу та не маєте книг у процесі. Якщо увімкнути це налаштування, то серії продовжуватимуться з останньої завершеної книги, а не з першої непочатої.", + "LabelSettingsParseSubtitles": "Дістати підзаголовки", + "LabelSettingsParseSubtitlesHelp": "Дістати підзаголовки з назв тек аудіокниг.<br>Підзаголовок мусить йти після \" - \"<br>Наприклад, \"Назва книги - Це підзаголовок\" має підзаголовок \"Це підзаголовок\"", + "LabelSettingsPreferMatchedMetadata": "Надавати перевагу віднайденим метаданим", + "LabelSettingsPreferMatchedMetadataHelp": "Подробиці буде перезаписано віднайденими даними Швидкого пошуку. Без цього Швидкий пошук заповнить лише подробиці, яких бракує.", + "LabelSettingsSkipMatchingBooksWithASIN": "Не шукати книги, що мають ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Не шукати книги, що мають ISBN", + "LabelSettingsSortingIgnorePrefixes": "Ігнорувати префікси при сортуванні", + "LabelSettingsSortingIgnorePrefixesHelp": "Наприклад, для префіксу \"1.\" назва книги \"1. Назва книги\" буде визначена як \"Назва книги, 1.\"", + "LabelSettingsSquareBookCovers": "Квадратні обкладинки", + "LabelSettingsSquareBookCoversHelp": "Надавати перевагу квадратним обкладинкам замість формату 1,6:1", + "LabelSettingsStoreCoversWithItem": "Зберігати обкладинки з елементом", + "LabelSettingsStoreCoversWithItemHelp": "За замовчуванням обкладинки зберігаються у /metadata/items. Цей параметр увімкне збереження обкладинок у теці елемента бібліотеки. Буде збережено лише один файл \"cover\"", + "LabelSettingsStoreMetadataWithItem": "Зберігати метадані з елементом", + "LabelSettingsStoreMetadataWithItemHelp": "За замовчуванням файли метаданих зберігаються у /metadata/items. Цей параметр увімкне збереження метаданих у теці елемента бібліотеки", + "LabelSettingsTimeFormat": "Формат часу", + "LabelShowAll": "Показати все", + "LabelSize": "Розмір", + "LabelSleepTimer": "Таймер вимкнення", + "LabelSlug": "Назва", + "LabelStart": "Початок", + "LabelStarted": "Почато", + "LabelStartedAt": "Почато", + "LabelStartTime": "Час початку", + "LabelStatsAudioTracks": "Аудіодоріжки", + "LabelStatsAuthors": "Автори", + "LabelStatsBestDay": "Найкращий день", + "LabelStatsDailyAverage": "В середньому за добу", + "LabelStatsDays": "Днів", + "LabelStatsDaysListened": "Днів прослухано", + "LabelStatsHours": "Годин", + "LabelStatsInARow": "поспіль", + "LabelStatsItemsFinished": "Елементів завершено", + "LabelStatsItemsInLibrary": "Елементів у бібліотеці", + "LabelStatsMinutes": "хвилин", + "LabelStatsMinutesListening": "Хвилин прослухано", + "LabelStatsOverallDays": "Днів загалом", + "LabelStatsOverallHours": "Годин загалом", + "LabelStatsWeekListening": "Прослухано за тиждень", + "LabelSubtitle": "Підзаголовок", + "LabelSupportedFileTypes": "Підтримувані типи файлів", + "LabelTag": "Мітка", + "LabelTags": "Мітки", + "LabelTagsAccessibleToUser": "Мітки, доступні користувачу", + "LabelTagsNotAccessibleToUser": "Мітки, недоступні користувачу", + "LabelTasks": "Запущені завдання", + "LabelTextEditorBulletedList": "Маркований список", + "LabelTextEditorLink": "Посилання", + "LabelTextEditorNumberedList": "Нумерований список", + "LabelTextEditorUnlink": "Прибрати посилання", + "LabelTheme": "Тема", + "LabelThemeDark": "Темна", + "LabelThemeLight": "Світла", + "LabelTimeBase": "Шкала часу", + "LabelTimeListened": "Часу прослухано", + "LabelTimeListenedToday": "Сьогодні прослухано", + "LabelTimeRemaining": "Залишилося: {0}", + "LabelTimeToShift": "На скільки секунд зсунути", + "LabelTitle": "Назва", + "LabelToolsEmbedMetadata": "Вбудувати метадані", + "LabelToolsEmbedMetadataDescription": "Вбудувати метадані в аудіофайли, включно з обкладинками та главами", + "LabelToolsMakeM4b": "Створити M4B-файл аудіокниги", + "LabelToolsMakeM4bDescription": "Створити .M4B-аудіокнигу з вбудованими метаданими, обкладинкою та главами.", + "LabelToolsSplitM4b": "Розділити M4B на MP3", + "LabelToolsSplitM4bDescription": "Створення MP3 з розділеного за главами M4B з вбудованими метаданими, обкладинкою та главами.", + "LabelTotalDuration": "Загальна тривалість", + "LabelTotalTimeListened": "Усього прослухано", + "LabelTrackFromFilename": "Доріжка за назвою файлу", + "LabelTrackFromMetadata": "Доріжка за метаданими", + "LabelTracks": "Доріжки", + "LabelTracksMultiTrack": "Декілька доріжок", + "LabelTracksNone": "Доріжки відсутні", + "LabelTracksSingleTrack": "Одна доріжка", + "LabelType": "Тип", + "LabelUnabridged": "Повна", + "LabelUndo": "Скасувати", + "LabelUnknown": "Невідомо", + "LabelUpdateCover": "Оновити обкладинку", + "LabelUpdateCoverHelp": "Дозволити перезапис наявних обкладинок обраних книг після віднайдення", + "LabelUpdatedAt": "Оновлення", + "LabelUpdateDetails": "Оновити подробиці", + "LabelUpdateDetailsHelp": "Дозволити перезапис наявних подробиць обраних книг після віднайдення", + "LabelUploaderDragAndDrop": "Перетягніть файли або теки", + "LabelUploaderDropFiles": "Перетягніть файли", + "LabelUploaderItemFetchMetadataHelp": "Автоматично шукати назву, автора та серію", + "LabelUseChapterTrack": "Прогрес глави", + "LabelUseFullTrack": "Використовувати доріжку повністю", + "LabelUser": "Користувач", + "LabelUsername": "Ім’я користувача", + "LabelValue": "Значення", + "LabelVersion": "Версія", + "LabelViewBookmarks": "Переглянути закладки", + "LabelViewChapters": "Переглянути глави", + "LabelViewQueue": "Переглянути чергу відтворення", + "LabelVolume": "Гучність", + "LabelWeekdaysToRun": "Виконувати у дні", + "LabelYearReviewHide": "Сховати підсумки року", + "LabelYearReviewShow": "Переглянути підсумки року", + "LabelYourAudiobookDuration": "Тривалість вашої аудіокниги", + "LabelYourBookmarks": "Ваші закладки", + "LabelYourPlaylists": "Ваші списки відтворення", + "LabelYourProgress": "Ваш прогрес", + "MessageAddToPlayerQueue": "Додати до черги відтворення", + "MessageAppriseDescription": "Щоб скористатися цією функцією, вам потрібно мати запущену <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> або API, що оброблятиме ті ж запити. <br />Аби надсилати сповіщення, URL-адреса API Apprise мусить бути повною, наприклад, якщо ваш API розміщено за адресою <code>http://192.168.1.1:8337</code>, то необхідно вказати адресу <code>http://192.168.1.1:8337/notify</code>.", + "MessageBackupsDescription": "Резервні копії містять користувачів, прогрес, подробиці елементів бібліотеки, налаштування сервера та зображення з <code>/metadata/items</code> та <code>/metadata/authors</code>. Резервні копії <strong>не</strong> містять жодних файлів з тек бібліотеки.", + "MessageBatchQuickMatchDescription": "Швидкий пошук спробує знайти відсутні обкладинки та метадані обраних елементів. Увімкніть налаштування нижче, аби дозволити заміну наявних обкладинок та/або метаданих під час швидкого пошуку.", + "MessageBookshelfNoCollections": "Ви не створили жодної добірки", + "MessageBookshelfNoResultsForFilter": "Немає результатів з фільтром \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "Немає відкритих RSS-каналів", + "MessageBookshelfNoSeries": "Серії відсутні", + "MessageChapterEndIsAfter": "Кінець глави знаходиться після закінчення книги", + "MessageChapterErrorFirstNotZero": "Перша глава мусить починатися з 0", + "MessageChapterErrorStartGteDuration": "Час початку мусить бути меншим за тривалість аудіокниги", + "MessageChapterErrorStartLtPrev": "Неприпустимий час початку, має бути більшим за час початку попередньої глави", + "MessageChapterStartIsAfter": "Початок глави знаходиться після закінчення книги", + "MessageCheckingCron": "Перевірка планувальника...", + "MessageConfirmCloseFeed": "Ви дійсно бажаєте закрити цей канал?", + "MessageConfirmDeleteBackup": "Ви дійсно бажаєте видалити резервну копію за {0}?", + "MessageConfirmDeleteFile": "Файл буде видалено з вашої файлової системи. Ви впевнені?", + "MessageConfirmDeleteLibrary": "Ви дійсно бажаєте назавжди видалити бібліотеку \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "Елемент бібліотеки буде видалено з бази даних та вашої файлової системи. Ви впевнені?", + "MessageConfirmDeleteLibraryItems": "З бази даних та вашої файлової системи будуть видалені елементи бібліотеки: {0}. Ви впевнені?", + "MessageConfirmDeleteSession": "Ви дійсно бажаєте видалити цей сеанс?", + "MessageConfirmForceReScan": "Ви дійсно бажаєте примусово пересканувати?", + "MessageConfirmMarkAllEpisodesFinished": "Ви дійсно бажаєте позначити усі епізоди завершеними?", + "MessageConfirmMarkAllEpisodesNotFinished": "Ви дійсно бажаєте позначити усі епізоди незавершеними?", + "MessageConfirmMarkSeriesFinished": "Ви дійсно бажаєте позначити усі книги серії завершеними?", + "MessageConfirmMarkSeriesNotFinished": "Ви дійсно бажаєте позначити всі книги серії незавершеними?", + "MessageConfirmQuickEmbed": "Увага! Швидке вбудування не створює резервних копій ваших аудіо. Переконайтеся, що маєте копію ваших файлів.<br><br>Продовжити?", + "MessageConfirmRemoveAllChapters": "Ви дійсно бажаєте видалити усі глави?", + "MessageConfirmRemoveAuthor": "Ви дійсно бажаєте видалити автора \"{0}\"?", + "MessageConfirmRemoveCollection": "Ви дійсно бажаєте видалити добірку \"{0}\"?", + "MessageConfirmRemoveEpisode": "Ви дійсно бажаєте видалити епізод \"{0}\"?", + "MessageConfirmRemoveEpisodes": "Ви дійсно бажаєте видалити епізодів: {0}?", + "MessageConfirmRemoveListeningSessions": "Ви дійсно бажаєте видалити сеанси прослуховування: {0}?", + "MessageConfirmRemoveNarrator": "Ви дійсно бажаєте видалити читця \"{0}\"?", + "MessageConfirmRemovePlaylist": "Ви дійсно бажаєте видалити список відтворення \"{0}\"?", + "MessageConfirmRenameGenre": "Ви дійсно бажаєте замінити жанр \"{0}\" на \"{1}\" для усіх елементів?", + "MessageConfirmRenameGenreMergeNote": "Примітка: такий жанр вже існує, тож їх буде об'єднано.", + "MessageConfirmRenameGenreWarning": "Увага! Вже існує схожий жанр у іншому регістрі \"{0}\".", + "MessageConfirmRenameTag": "Ви дійсно бажаєте замінити мітку \"{0}\" на \"{1}\" для усіх елементів?", + "MessageConfirmRenameTagMergeNote": "Примітка: така мітка вже існує, тож їх буде об'єднано.", + "MessageConfirmRenameTagWarning": "Увага! Вже існує схожа мітка у іншому регістрі \"{0}\".", + "MessageConfirmReScanLibraryItems": "Ви дійсно бажаєте пересканувати елементи: {0}?", + "MessageConfirmSendEbookToDevice": "Ви дійсно хочете відправити на пристрій \"{2}\" електроні книги: {0}, \"{1}\"?", + "MessageDownloadingEpisode": "Завантаження епізоду", + "MessageDragFilesIntoTrackOrder": "Перетягніть файли до правильного порядку", + "MessageEmbedFinished": "Вбудовано!", + "MessageEpisodesQueuedForDownload": "Епізодів у черзі завантаження: {0}", + "MessageFeedURLWillBe": "URL-адреса каналу буде {0}", + "MessageFetching": "Отримання...", + "MessageForceReScanDescription": "Просканує усі файли заново, неначе вперше. ID3-мітки, файли OPF та текстові файли будуть проскановані як нові.", + "MessageImportantNotice": "Важливе повідомлення!", + "MessageInsertChapterBelow": "Введіть главу нижче", + "MessageItemsSelected": "Обрано елементів: {0}", + "MessageItemsUpdated": "Оновлено елементів: {0}", + "MessageJoinUsOn": "Приєднуйтесь до", + "MessageListeningSessionsInTheLastYear": "Сесій прослуховування минулого року: {0}", + "MessageLoading": "Завантаження...", + "MessageLoadingFolders": "Завантаження тек...", + "MessageM4BFailed": "Помилка M4B!", + "MessageM4BFinished": "M4B створено!", + "MessageMapChapterTitles": "Встановіть назви глав вашої аудіокниги без визначення налаштувань тривалості", + "MessageMarkAllEpisodesFinished": "Позначити всі епізоди завершеними", + "MessageMarkAllEpisodesNotFinished": "Позначити всі епізоди незавершеними", + "MessageMarkAsFinished": "Позначити завершеним", + "MessageMarkAsNotFinished": "Позначити незавершеним", + "MessageMatchBooksDescription": "Спробує віднайти книгу у вказаному джерелі пошуку та встановити подробиці та обкладинку, яких бракує. Не перезаписує подробиці.", + "MessageNoAudioTracks": "Аудіодоріжки відсутні", + "MessageNoAuthors": "Автори відсутні", + "MessageNoBackups": "Резервні копії відсутні", + "MessageNoBookmarks": "Немає закладок", + "MessageNoChapters": "Глави відсутні", + "MessageNoCollections": "Добірки відсутні", + "MessageNoCoversFound": "Обкладинок не знайдено", + "MessageNoDescription": "Без опису", + "MessageNoDownloadsInProgress": "Немає активних завантажень", + "MessageNoDownloadsQueued": "Немає завантажень у черзі", + "MessageNoEpisodeMatchesFound": "Відповідних епізодів не знайдено", + "MessageNoEpisodes": "Епізоди відсутні", + "MessageNoFoldersAvailable": "Немає доступних тек", + "MessageNoGenres": "Без жанру", + "MessageNoIssues": "Немає проблем", + "MessageNoItems": "Елементи відсутні", + "MessageNoItemsFound": "Елементів не знайдено", + "MessageNoListeningSessions": "Сеанси прослуховування відсутні", + "MessageNoLogs": "Журнал порожній", + "MessageNoMediaProgress": "Прогрес відсутній", + "MessageNoNotifications": "Сповіщення відсутні", + "MessageNoPodcastsFound": "Подкастів не знайдено", + "MessageNoResults": "Немає результатів", + "MessageNoSearchResultsFor": "Немає результатів пошуку для \"{0}\"", + "MessageNoSeries": "Без серії", + "MessageNoTags": "Без міток", + "MessageNoTasksRunning": "Немає активних завдань", + "MessageNotYetImplemented": "Ще не реалізовано", + "MessageNoUpdateNecessary": "Оновлення не потрібно", + "MessageNoUpdatesWereNecessary": "Оновлень не потрібно", + "MessageNoUserPlaylists": "У вас немає списків відтворення", + "MessageOr": "або", + "MessagePauseChapter": "Призупинити відтворення глави", + "MessagePlayChapter": "Слухати початок глави", + "MessagePlaylistCreateFromCollection": "Створити список відтворення з добірки", + "MessagePodcastHasNoRSSFeedForMatching": "Подкаст не має RSS-каналу для пошуку", + "MessageQuickMatchDescription": "Заповнити відсутні подробиці та обкладинку першим результатом пошуку '{0}'. Не перезаписує подробиці, якщо не увімкнено параметр \"Надавати перевагу віднайденим метаданим\".", + "MessageRemoveChapter": "Видалити главу", + "MessageRemoveEpisodes": "Видалити епізодів: {0}", + "MessageRemoveFromPlayerQueue": "Вилучити з черги відтворення", + "MessageRemoveUserWarning": "Ви дійсно бажаєте назавжди видалити користувача \"{0}\"?", + "MessageReportBugsAndContribute": "Повідомляйте про помилки, пропонуйте функції та долучайтеся на", + "MessageResetChaptersConfirm": "Ви дійсно бажаєте скинути глави та скасувати внесені зміни?", + "MessageRestoreBackupConfirm": "Ви дійсно бажаєте відновити резервну копію від", + "MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних, розташовану в /config, і зображення обкладинок в /metadata/items та /metadata/authors.<br /><br />Резервні копії не змінюють жодних файлів у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються..<br /><br />Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.", + "MessageSearchResultsFor": "Результати пошуку для", + "MessageSelected": "Вибрано: {0}", + "MessageServerCouldNotBeReached": "Не вдалося підключитися до сервера", + "MessageSetChaptersFromTracksDescription": "Створити глави з аудіодоріжок, встановивши назви файлів за заголовки", + "MessageStartPlaybackAtTime": "Почати відтворення \"{0}\" з {1}?", + "MessageThinking": "Думаю…", + "MessageUploaderItemFailed": "Не вдалося завантажити", + "MessageUploaderItemSuccess": "Успішно завантажено!", + "MessageUploading": "Завантаження...", + "MessageValidCronExpression": "Допустима команда cron", + "MessageWatcherIsDisabledGlobally": "Спостерігача вимкнено в налаштуваннях сервера", + "MessageXLibraryIsEmpty": "Бібліотека {0} порожня!", + "MessageYourAudiobookDurationIsLonger": "Тривалість вашої аудіокниги довша за віднайдену", + "MessageYourAudiobookDurationIsShorter": "Тривалість вашої аудіокниги коротша за віднайдену", + "NoteChangeRootPassword": "Кореневий користувач — єдиний, хто може мати порожній пароль", + "NoteChapterEditorTimes": "Примітка: Перша глава мусить починатися з 0:00, а час початку останньої глави не може бути більшим за зазначену тривалість аудіокниги.", + "NoteFolderPicker": "Примітка: вже обрані теки не буде показано", + "NoteRSSFeedPodcastAppsHttps": "Попередження: Більшість додатків подкастів вимагатимуть використання протоколу HTTPS від RSS-каналу", + "NoteRSSFeedPodcastAppsPubDate": "Попередження: 1 або більше ваших епізодів не мають дати публікації. Деякі додатки подкастів вимагають це.", + "NoteUploaderFoldersWithMediaFiles": "Теки з медіафайлами буде оброблено як окремі елементи бібліотеки.", + "NoteUploaderOnlyAudioFiles": "Якщо завантажувати лише аудіофайли, то кожен файл буде оброблено як окрему книгу.", + "NoteUploaderUnsupportedFiles": "Непідтримувані файли пропущено. Під час вибору або перетягування теки, файли, що знаходяться поза текою, пропускаються.", + "PlaceholderNewCollection": "Нова назва добірки", + "PlaceholderNewFolderPath": "Новий шлях до теки", + "PlaceholderNewPlaylist": "Нова назва списку", + "PlaceholderSearch": "Пошук...", + "PlaceholderSearchEpisode": "Шукати епізод...", + "ToastAccountUpdateFailed": "Не вдалося оновити профіль", + "ToastAccountUpdateSuccess": "Профіль оновлено", + "ToastAuthorImageRemoveFailed": "Не вдалося видалити зображення", + "ToastAuthorImageRemoveSuccess": "Фото автора видалено", + "ToastAuthorUpdateFailed": "Не вдалося оновити автора", + "ToastAuthorUpdateMerged": "Автора об'єднано", + "ToastAuthorUpdateSuccess": "Автора оновлено", + "ToastAuthorUpdateSuccessNoImageFound": "Автора оновлено (фото не знайдено)", + "ToastBackupCreateFailed": "Не вдалося створити резервну копію", + "ToastBackupCreateSuccess": "Резервну копію створено", + "ToastBackupDeleteFailed": "Не вдалося видалити резервну копію", + "ToastBackupDeleteSuccess": "Резервну копію видалено", + "ToastBackupRestoreFailed": "Не вдалося відновити резервну копію", + "ToastBackupUploadFailed": "Не вдалося завантажити резервну копію", + "ToastBackupUploadSuccess": "Резервну копію завантажено", + "ToastBatchUpdateFailed": "Не вдалося оновити обрані", + "ToastBatchUpdateSuccess": "Обрані успішно оновлено", + "ToastBookmarkCreateFailed": "Не вдалося створити закладку", + "ToastBookmarkCreateSuccess": "Закладку додано", + "ToastBookmarkRemoveFailed": "Не вдалося видалити закладку", + "ToastBookmarkRemoveSuccess": "Закладку видалено", + "ToastBookmarkUpdateFailed": "Не вдалося оновити закладку", + "ToastBookmarkUpdateSuccess": "Закладку оновлено", + "ToastChaptersHaveErrors": "Глави містять помилки", + "ToastChaptersMustHaveTitles": "Глави повинні мати назви", + "ToastCollectionItemsRemoveFailed": "Не вдалося видалити елемент(и) з добірки", + "ToastCollectionItemsRemoveSuccess": "Елемент(и) видалено з добірки", + "ToastCollectionRemoveFailed": "Не вдалося видалити добірку", + "ToastCollectionRemoveSuccess": "Добірку видалено", + "ToastCollectionUpdateFailed": "Не вдалося оновити добірку", + "ToastCollectionUpdateSuccess": "Добірку оновлено", + "ToastItemCoverUpdateFailed": "Не вдалося оновити обкладинку", + "ToastItemCoverUpdateSuccess": "Обкладинку елемента оновлено", + "ToastItemDetailsUpdateFailed": "Не вдалося оновити подробиці елемента", + "ToastItemDetailsUpdateSuccess": "Подробиці про елемент оновлено", + "ToastItemDetailsUpdateUnneeded": "Оновлення подробиць непотрібне", + "ToastItemMarkedAsFinishedFailed": "Не вдалося позначити як завершене", + "ToastItemMarkedAsFinishedSuccess": "Елемент позначено як завершений", + "ToastItemMarkedAsNotFinishedFailed": "Не вдалося позначити незавершеним", + "ToastItemMarkedAsNotFinishedSuccess": "Елемент позначено незавершеним", + "ToastLibraryCreateFailed": "Не вдалося створити бібліотеку", + "ToastLibraryCreateSuccess": "Бібліотеку \"{0}\" створено", + "ToastLibraryDeleteFailed": "Не вдалося видалити бібліотеку", + "ToastLibraryDeleteSuccess": "Бібліотеку видалено", + "ToastLibraryScanFailedToStart": "Не вдалося розпочати сканування", + "ToastLibraryScanStarted": "Почалося сканування бібліотеки", + "ToastLibraryUpdateFailed": "Не вдалося оновити бібліотеку", + "ToastLibraryUpdateSuccess": "Бібліотеку \"{0}\" оновлено", + "ToastPlaylistCreateFailed": "Не вдалося створити список", + "ToastPlaylistCreateSuccess": "Список відтворення створено", + "ToastPlaylistRemoveFailed": "Не вдалося видалити список", + "ToastPlaylistRemoveSuccess": "Список відтворення видалено", + "ToastPlaylistUpdateFailed": "Не вдалося оновити список", + "ToastPlaylistUpdateSuccess": "Список відтворення оновлено", + "ToastPodcastCreateFailed": "Не вдалося створити подкаст", + "ToastPodcastCreateSuccess": "Подкаст успішно створено", + "ToastRemoveItemFromCollectionFailed": "Не вдалося видалити елемент із добірки", + "ToastRemoveItemFromCollectionSuccess": "Елемент видалено з добірки", + "ToastRSSFeedCloseFailed": "Не вдалося закрити RSS-канал", + "ToastRSSFeedCloseSuccess": "RSS-канал закрито", + "ToastSendEbookToDeviceFailed": "Не вдалося надіслати електронну книгу на пристрій", + "ToastSendEbookToDeviceSuccess": "Електронну книгу надіслано на пристрій \"{0}\"", + "ToastSeriesUpdateFailed": "Не вдалося оновити серію", + "ToastSeriesUpdateSuccess": "Серію успішно оновлено", + "ToastSessionDeleteFailed": "Не вдалося видалити сесію", + "ToastSessionDeleteSuccess": "Сесію видалено", + "ToastSocketConnected": "Сокет під'єднано", + "ToastSocketDisconnected": "Сокет від'єднано", + "ToastSocketFailedToConnect": "Не вдалося під'єднатися до сокета", + "ToastUserDeleteFailed": "Не вдалося видалити користувача", + "ToastUserDeleteSuccess": "Користувача видалено" +} From b0a9bed15a0dbae6eab9d616bb617f37eb14a5dd Mon Sep 17 00:00:00 2001 From: Illia Pyshniak <corbee@pm.me> Date: Tue, 19 Mar 2024 23:18:34 +0200 Subject: [PATCH 0469/2145] Update i18n.js Add Ukrainian language and podcast region --- client/plugins/i18n.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 0ee0e5b1..ff7af170 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -23,6 +23,7 @@ const languageCodeMap = { 'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' }, 'ru': { label: 'Русский', dateFnsLocale: 'ru' }, 'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, + 'uk': { label: 'Українська', dateFnsLocale: 'uk' }, 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, 'zh-tw': { label: '正體中文 (Traditional Chinese)', dateFnsLocale: 'zhTW' } @@ -36,6 +37,7 @@ Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { // iTunes search API uses ISO 3166 country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 const podcastSearchRegionMap = { + 'ua': { label: 'Україна' }, 'us': { label: 'United States' }, 'cn': { label: '中国' } } From 1bee0827200d95d2cf7839bff99b46cf21dc5ac6 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 20 Mar 2024 11:40:50 +0200 Subject: [PATCH 0470/2145] Update libraryFolderID correctly in scanFolderUpdates --- server/scanner/LibraryItemScanner.js | 11 ++++++----- server/scanner/LibraryScanner.js | 9 +++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/server/scanner/LibraryItemScanner.js b/server/scanner/LibraryItemScanner.js index 588b7744..872000d8 100644 --- a/server/scanner/LibraryItemScanner.js +++ b/server/scanner/LibraryItemScanner.js @@ -21,10 +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 + * @param {{relPath:string, path:string}} [updateLibraryItemDetails] used by watcher when item folder was renamed * @returns {number} ScanResult */ - async scanLibraryItem(libraryItemId, renamedPaths = null) { + async scanLibraryItem(libraryItemId, updateLibraryItemDetails = null) { // TODO: Add task manager const libraryItem = await Database.libraryItemModel.findByPk(libraryItemId) if (!libraryItem) { @@ -32,11 +32,12 @@ class LibraryItemScanner { return ScanResult.NOTHING } + const libraryFolderId = updateLibraryItemDetails?.libraryFolderId || libraryItem.libraryFolderId const library = await Database.libraryModel.findByPk(libraryItem.libraryId, { include: { model: Database.libraryFolderModel, where: { - id: libraryItem.libraryFolderId + id: libraryFolderId } } }) @@ -51,9 +52,9 @@ class LibraryItemScanner { const scanLogger = new ScanLogger() scanLogger.verbose = true - scanLogger.setData('libraryItem', renamedPaths?.relPath || libraryItem.relPath) + scanLogger.setData('libraryItem', updateLibraryItemDetails?.relPath || libraryItem.relPath) - const libraryItemPath = renamedPaths?.path || fileUtils.filePathToPOSIX(libraryItem.path) + const libraryItemPath = updateLibraryItemDetails?.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 27c507bd..ac422c79 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -525,7 +525,7 @@ class LibraryScanner { path: potentialChildDirs }) - let renamedPaths = {} + let updatedLibraryItemDetails = {} if (!existingLibraryItem) { const dirIno = await fileUtils.getIno(fullPath) existingLibraryItem = await Database.libraryItemModel.findOneOld({ @@ -536,8 +536,9 @@ class LibraryScanner { // Update library item paths for scan existingLibraryItem.path = fullPath existingLibraryItem.relPath = itemDir - renamedPaths.path = fullPath - renamedPaths.relPath = itemDir + updatedLibraryItemDetails.path = fullPath + updatedLibraryItemDetails.relPath = itemDir + updatedLibraryItemDetails.libraryFolderId = folder.id } } if (existingLibraryItem) { @@ -557,7 +558,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, renamedPaths) + itemGroupingResults[itemDir] = await LibraryItemScanner.scanLibraryItem(existingLibraryItem.id, updatedLibraryItemDetails) continue } else if (library.settings.audiobooksOnly && !hasAudioFiles(fileUpdateGroup, itemDir)) { Logger.debug(`[LibraryScanner] Folder update for relative path "${itemDir}" has no audio files`) From 01c8d42291b97fdd770290e2ae70e2d433874f45 Mon Sep 17 00:00:00 2001 From: burghy86 <burghy@mail.com> Date: Thu, 21 Mar 2024 12:52:49 +0100 Subject: [PATCH 0471/2145] Update it.json month fix string --- client/strings/it.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/strings/it.json b/client/strings/it.json index 8924a28b..bd6e5fa9 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -43,7 +43,7 @@ "ButtonMatchAllAuthors": "Aggiungi metadata agli Autori", "ButtonMatchBooks": "Aggiungi metadata della Libreria", "ButtonNevermind": "Nevermind", - "ButtonNext": "Next", + "ButtonNext": "Prossimo", "ButtonNextChapter": "Prossimo Capitolo", "ButtonOk": "Ok", "ButtonOpenFeed": "Apri Feed", @@ -52,7 +52,7 @@ "ButtonPlay": "Play", "ButtonPlaying": "In Riproduzione", "ButtonPlaylists": "Playlists", - "ButtonPrevious": "Previous", + "ButtonPrevious": "Precendente", "ButtonPreviousChapter": "Capitolo Precendente", "ButtonPurgeAllCache": "Elimina tutta la Cache", "ButtonPurgeItemsCache": "Elimina la Cache selezionata", @@ -113,7 +113,7 @@ "HeaderCollectionItems": "Elementi della Raccolta", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Download Correnti", - "HeaderCustomMetadataProviders": "Custom Metadata Providers", + "HeaderCustomMetadataProviders": " Metadata Providers Personalizzato", "HeaderDetails": "Dettagli", "HeaderDownloadQueue": "Download coda", "HeaderEbookFiles": "Ebook File", @@ -184,7 +184,7 @@ "HeaderUpdateDetails": "Aggiorna Dettagli", "HeaderUpdateLibrary": "Aggiorna Libreria", "HeaderUsers": "Utenti", - "HeaderYearReview": "Year {0} in Review", + "HeaderYearReview": "Anno {0} in Sintesi", "HeaderYourStats": "Statistiche Personali", "LabelAbridged": "Abbreviato", "LabelAccountType": "Tipo di Account", @@ -356,9 +356,9 @@ "LabelMetaTags": "Meta Tags", "LabelMinute": "Minuto", "LabelMissing": "Altro", - "LabelMissingEbook": "Has no ebook", + "LabelMissingEbook": "Non ha ebook", "LabelMissingParts": "Parti rimanenti", - "LabelMissingSupplementaryEbook": "Has no supplementary ebook", + "LabelMissingSupplementaryEbook": "Non ha ebook supplementare", "LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti", "LabelMobileRedirectURIsDescription": "Questa è una lista bianca di URI di reindirizzamento validi per le app mobili. Quello predefinito è <code>audiobookshelf://oauth</code>, che puoi rimuovere o integrare con URI aggiuntivi per l'integrazione di app di terze parti. Utilizzando un asterisco (<code>*</code>) poiché l'unica voce consente qualsiasi URI.", "LabelMore": "Molto", @@ -444,7 +444,7 @@ "LabelSeries": "Serie", "LabelSeriesName": "Nome Serie", "LabelSeriesProgress": "Cominciato", - "LabelServerYearReview": "Server Year in Review ({0})", + "LabelServerYearReview": "Anno del server in sintesi({0})", "LabelSetEbookAsPrimary": "Immposta come Primario", "LabelSetEbookAsSupplementary": "Imposta come Suplementare", "LabelSettingsAudiobooksOnly": "Solo Audiolibri", @@ -778,4 +778,4 @@ "ToastSocketFailedToConnect": "Socket non riesce a connettersi", "ToastUserDeleteFailed": "Errore eliminazione utente", "ToastUserDeleteSuccess": "Utente eliminato" -} \ No newline at end of file +} From 8d7530254c5dca603e754aede713ea015cefa77d Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 21 Mar 2024 14:23:49 -0500 Subject: [PATCH 0472/2145] Update:Re-order chapters table infront of audio tracks table on book item page #2778 --- client/pages/item/_id/index.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index d568d534..8c78d97d 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -137,12 +137,12 @@ <p v-for="audioFile in invalidAudioFiles" :key="audioFile.id" class="text-xs pl-2">- {{ audioFile.metadata.filename }} ({{ audioFile.error }})</p> </div> + <tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" /> + <widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" /> <tables-podcast-lazy-episodes-table v-if="isPodcast" :library-item="libraryItem" /> - <tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" /> - <tables-ebook-files-table v-if="ebookFiles.length" :library-item="libraryItem" class="mt-6" /> <tables-library-files-table v-if="libraryFiles.length" :library-item="libraryItem" class="mt-6" /> From ff5226fa9369b599b2b10cc2c1fe7579a1ceb902 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 21 Mar 2024 14:38:52 -0500 Subject: [PATCH 0473/2145] Update:Remove unused missing/invalid audiobook parts logic and keys --- client/components/cards/LazyBookCard.vue | 20 +---- client/components/widgets/AudiobookData.vue | 84 --------------------- client/pages/audiobook/_id/chapters.vue | 2 +- client/pages/audiobook/_id/edit.vue | 3 - client/pages/audiobook/_id/manage.vue | 2 +- client/pages/item/_id/index.vue | 18 ++--- client/strings/cs.json | 2 - client/strings/da.json | 2 - client/strings/de.json | 2 - client/strings/en-us.json | 2 - client/strings/es.json | 2 - client/strings/et.json | 2 - client/strings/fr.json | 2 - client/strings/gu.json | 2 - client/strings/he.json | 2 - client/strings/hi.json | 2 - client/strings/hr.json | 2 - client/strings/hu.json | 2 - client/strings/it.json | 4 +- client/strings/lt.json | 2 - client/strings/nl.json | 2 - client/strings/no.json | 2 - client/strings/pl.json | 2 - client/strings/pt-br.json | 4 +- client/strings/ru.json | 2 - client/strings/sv.json | 2 - client/strings/uk.json | 4 +- client/strings/vi-vn.json | 2 - client/strings/zh-cn.json | 2 - client/strings/zh-tw.json | 6 +- server/models/Book.js | 2 +- server/objects/files/AudioFile.js | 7 -- server/objects/mediaTypes/Book.js | 13 +--- 33 files changed, 19 insertions(+), 190 deletions(-) delete mode 100644 client/components/widgets/AudiobookData.vue diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 42b020e3..efeb0165 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -358,7 +358,7 @@ export default { }, showError() { if (this.recentEpisode) return false // Dont show podcast error on episode card - return this.numInvalidAudioFiles || this.numMissingParts || this.isMissing || this.isInvalid + return this.isMissing || this.isInvalid }, libraryItemIdStreaming() { return this.store.getters['getLibraryItemIdStreaming'] @@ -388,29 +388,13 @@ export default { isInvalid() { return this._libraryItem.isInvalid }, - numMissingParts() { - if (this.isPodcast) return 0 - return this.media.numMissingParts - }, - numInvalidAudioFiles() { - if (this.isPodcast) return 0 - return this.media.numInvalidAudioFiles - }, errorText() { if (this.isMissing) return 'Item directory is missing!' else if (this.isInvalid) { if (this.isPodcast) return 'Podcast has no episodes' return 'Item has no audio tracks & ebook' } - let txt = '' - if (this.numMissingParts) { - txt += `${this.numMissingParts} missing parts.` - } - if (this.numInvalidAudioFiles) { - if (txt) txt += ' ' - txt += `${this.numInvalidAudioFiles} invalid audio files.` - } - return txt || 'Unknown Error' + return 'Unknown Error' }, overlayWrapperClasslist() { const classes = [] diff --git a/client/components/widgets/AudiobookData.vue b/client/components/widgets/AudiobookData.vue deleted file mode 100644 index 8309faca..00000000 --- a/client/components/widgets/AudiobookData.vue +++ /dev/null @@ -1,84 +0,0 @@ -<template> - <div class="w-full"> - <div v-if="missingParts.length" class="bg-error border-red-800 shadow-md p-4"> - <p class="text-sm mb-2"> - {{ $strings.LabelMissingParts }} <span class="text-sm">({{ missingParts.length }})</span> - </p> - <p class="text-sm font-mono">{{ missingPartChunks.join(', ') }}</p> - </div> - - <div v-if="invalidParts.length" class="bg-error border-red-800 shadow-md p-4"> - <p class="text-sm mb-2"> - {{ $strings.LabelInvalidParts }} <span class="text-sm">({{ invalidParts.length }})</span> - </p> - <div> - <p v-for="part in invalidParts" :key="part.filename" class="text-sm font-mono">{{ part.filename }}: {{ part.error }}</p> - </div> - </div> - - <tables-tracks-table :title="$strings.LabelStatsAudioTracks" :tracks="tracksWithAudioFile" :is-file="isFile" :library-item-id="libraryItemId" class="mt-6" /> - </div> -</template> - -<script> -export default { - props: { - libraryItemId: String, - media: { - type: Object, - default: () => {} - }, - isFile: Boolean - }, - data() { - return {} - }, - computed: { - tracksWithAudioFile() { - return this.media.tracks.map((track) => { - track.audioFile = this.media.audioFiles.find((af) => af.metadata.path === track.metadata.path) - return track - }) - }, - missingPartChunks() { - if (this.missingParts === 1) return this.missingParts[0] - var chunks = [] - - var currentIndex = this.missingParts[0] - var currentChunk = [this.missingParts[0]] - - for (let i = 1; i < this.missingParts.length; i++) { - var partIndex = this.missingParts[i] - if (currentIndex === partIndex - 1) { - currentChunk.push(partIndex) - currentIndex = partIndex - } else { - // console.log('Chunk ended', currentChunk.join(', '), currentIndex, partIndex) - if (currentChunk.length === 0) { - console.error('How is current chunk 0?', currentChunk.join(', ')) - } - chunks.push(currentChunk) - currentChunk = [partIndex] - currentIndex = partIndex - } - } - if (currentChunk.length) { - chunks.push(currentChunk) - } - chunks = chunks.map((chunk) => { - if (chunk.length === 1) return chunk[0] - else return `${chunk[0]}-${chunk[chunk.length - 1]}` - }) - return chunks - }, - missingParts() { - return this.media.missingParts || [] - }, - invalidParts() { - return this.media.invalidParts || [] - } - }, - methods: {}, - mounted() {} -} -</script> \ No newline at end of file diff --git a/client/pages/audiobook/_id/chapters.vue b/client/pages/audiobook/_id/chapters.vue index 0f5db77a..ef4bbee4 100644 --- a/client/pages/audiobook/_id/chapters.vue +++ b/client/pages/audiobook/_id/chapters.vue @@ -281,7 +281,7 @@ export default { return this.media.audioFiles || [] }, audioTracks() { - return this.audioFiles.filter((af) => !af.exclude && !af.invalid) + return this.audioFiles.filter((af) => !af.exclude) }, selectedChapterId() { return this.selectedChapter ? this.selectedChapter.id : null diff --git a/client/pages/audiobook/_id/edit.vue b/client/pages/audiobook/_id/edit.vue index f6d4879d..69e96bf8 100644 --- a/client/pages/audiobook/_id/edit.vue +++ b/client/pages/audiobook/_id/edit.vue @@ -137,9 +137,6 @@ export default { }) return count }, - missingParts() { - return this.media.missingParts || [] - }, libraryItemId() { return this.libraryItem.id }, diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index 965f8c25..03c214b4 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -249,7 +249,7 @@ export default { return this.media.metadata || {} }, audioFiles() { - return (this.media.audioFiles || []).filter((af) => !af.exclude && !af.invalid) + return (this.media.audioFiles || []).filter((af) => !af.exclude) }, isSingleM4b() { return this.audioFiles.length === 1 && this.audioFiles[0].metadata.ext.toLowerCase() === '.m4b' diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 8c78d97d..b4b58bda 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -131,15 +131,9 @@ </button> </div> - <div v-if="invalidAudioFiles.length" class="bg-error border-red-800 shadow-md p-4"> - <p class="text-sm mb-2">Invalid audio files</p> - - <p v-for="audioFile in invalidAudioFiles" :key="audioFile.id" class="text-xs pl-2">- {{ audioFile.metadata.filename }} ({{ audioFile.error }})</p> - </div> - <tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" /> - <widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" /> + <tables-tracks-table v-if="tracks.length" :title="$strings.LabelStatsAudioTracks" :tracks="tracksWithAudioFile" :is-file="isFile" :library-item-id="libraryItemId" class="mt-6" /> <tables-podcast-lazy-episodes-table v-if="isPodcast" :library-item="libraryItem" /> @@ -239,10 +233,6 @@ export default { isAbridged() { return !!this.mediaMetadata.abridged }, - invalidAudioFiles() { - if (!this.isBook) return [] - return this.libraryItem.media.audioFiles.filter((af) => af.invalid) - }, showPlayButton() { if (this.isMissing || this.isInvalid) return false if (this.isMusic) return !!this.audioFile @@ -275,6 +265,12 @@ export default { tracks() { return this.media.tracks || [] }, + tracksWithAudioFile() { + return this.tracks.map((track) => { + track.audioFile = this.media.audioFiles?.find((af) => af.metadata.path === track.metadata.path) + return track + }) + }, podcastEpisodes() { return this.media.episodes || [] }, diff --git a/client/strings/cs.json b/client/strings/cs.json index 4ce358a2..6e935617 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Každých 6 hodin", "LabelIntervalEveryDay": "Každý den", "LabelIntervalEveryHour": "Každou hodinu", - "LabelInvalidParts": "Neplatné části", "LabelInvert": "Invertovat", "LabelItem": "Položka", "LabelLanguage": "Jazyk", @@ -357,7 +356,6 @@ "LabelMinute": "Minuta", "LabelMissing": "Chybějící", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Chybějící díly", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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.", diff --git a/client/strings/da.json b/client/strings/da.json index de00a1fc..ea69844c 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Hver 6. time", "LabelIntervalEveryDay": "Hver dag", "LabelIntervalEveryHour": "Hver time", - "LabelInvalidParts": "Ugyldige dele", "LabelInvert": "Inverter", "LabelItem": "Element", "LabelLanguage": "Sprog", @@ -357,7 +356,6 @@ "LabelMinute": "Minut", "LabelMissing": "Mangler", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Manglende dele", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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.", diff --git a/client/strings/de.json b/client/strings/de.json index ed99f095..207bc399 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Alle 6 Stunden", "LabelIntervalEveryDay": "Jeden Tag", "LabelIntervalEveryHour": "Jede Stunde", - "LabelInvalidParts": "Ungültige Teile", "LabelInvert": "Umkehren", "LabelItem": "Medium", "LabelLanguage": "Sprache", @@ -357,7 +356,6 @@ "LabelMinute": "Minute", "LabelMissing": "Fehlend", "LabelMissingEbook": "E-Book fehlt", - "LabelMissingParts": "Fehlende Teile", "LabelMissingSupplementaryEbook": "Ergänzendes E-Book fehlt", "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 du entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen kannst. Die Verwendung eines Sternchens (<code>*</code>) als alleiniger Eintrag erlaubt jede URI.", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 43a1ef44..bba4669b 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Every 6 hours", "LabelIntervalEveryDay": "Every day", "LabelIntervalEveryHour": "Every hour", - "LabelInvalidParts": "Invalid Parts", "LabelInvert": "Invert", "LabelItem": "Item", "LabelLanguage": "Language", @@ -357,7 +356,6 @@ "LabelMinute": "Minute", "LabelMissing": "Missing", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Missing Parts", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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.", diff --git a/client/strings/es.json b/client/strings/es.json index 068c5bc4..b791a472 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Cada 6 Horas", "LabelIntervalEveryDay": "Cada Día", "LabelIntervalEveryHour": "Cada Hora", - "LabelInvalidParts": "Partes Inválidas", "LabelInvert": "Invertir", "LabelItem": "Elemento", "LabelLanguage": "Lenguaje", @@ -357,7 +356,6 @@ "LabelMinute": "Minuto", "LabelMissing": "Ausente", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Partes Ausentes", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos", "LabelMobileRedirectURIsDescription": "Esta es una lista de URIs válidos para redireccionamiento de apps móviles. La URI por defecto es <code>audiobookshelf://oauth</code>, la cual puedes remover or corroborar con URIs adicionales para la integración con apps de terceros. Utilizando un asterisco (<code>*</code>) como el único punto de entrada permite cualquier URI.", diff --git a/client/strings/et.json b/client/strings/et.json index da145027..9aa0d0db 100644 --- a/client/strings/et.json +++ b/client/strings/et.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Iga 6 tunni tagant", "LabelIntervalEveryDay": "Iga päev", "LabelIntervalEveryHour": "Iga tunni tagant", - "LabelInvalidParts": "Vigased osad", "LabelInvert": "Pööra ümber", "LabelItem": "Kirje", "LabelLanguage": "Keel", @@ -357,7 +356,6 @@ "LabelMinute": "Minut", "LabelMissing": "Puudub", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Puuduvad osad", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Lubatud mobiilile suunamise URI-d", "LabelMobileRedirectURIsDescription": "See on mobiilirakenduste jaoks kehtivate suunamise URI-de lubatud nimekiri. Vaikimisi on selleks <code>audiobookshelf://oauth</code>, mida saate eemaldada või täiendada täiendavate URI-dega kolmanda osapoole rakenduste integreerimiseks. Tärni (<code>*</code>) ainukese kirjena kasutamine võimaldab mis tahes URI-d.", diff --git a/client/strings/fr.json b/client/strings/fr.json index b7b28472..516a2f32 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Toutes les 6 heures", "LabelIntervalEveryDay": "Tous les jours", "LabelIntervalEveryHour": "Toutes les heures", - "LabelInvalidParts": "Parties invalides", "LabelInvert": "Inverser", "LabelItem": "Article", "LabelLanguage": "Langue", @@ -357,7 +356,6 @@ "LabelMinute": "Minute", "LabelMissing": "Manquant", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Parties manquantes", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "URI de redirection mobile autorisés", "LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche d’URI de redirection valides pour les applications mobiles. Celui par défaut est <code>audiobookshelf://oauth</code>, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l'intégration d'applications tierces. L’utilisation d’un astérisque (<code>*</code>) comme seule entrée autorise n’importe quel URI.", diff --git a/client/strings/gu.json b/client/strings/gu.json index 0a2dc3b6..0cf6d86d 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Every 6 hours", "LabelIntervalEveryDay": "Every day", "LabelIntervalEveryHour": "Every hour", - "LabelInvalidParts": "Invalid Parts", "LabelInvert": "Invert", "LabelItem": "Item", "LabelLanguage": "Language", @@ -357,7 +356,6 @@ "LabelMinute": "Minute", "LabelMissing": "Missing", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Missing Parts", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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.", diff --git a/client/strings/he.json b/client/strings/he.json index 435f60ad..63bd3339 100644 --- a/client/strings/he.json +++ b/client/strings/he.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "כל 6 שעות", "LabelIntervalEveryDay": "כל יום", "LabelIntervalEveryHour": "כל שעה", - "LabelInvalidParts": "חלקים לא תקינים", "LabelInvert": "הפוך", "LabelItem": "פריט", "LabelLanguage": "שפה", @@ -357,7 +356,6 @@ "LabelMinute": "דקה", "LabelMissing": "חסר", "LabelMissingEbook": "אין ספר אלקטרוני", - "LabelMissingParts": "חלקים חסרים", "LabelMissingSupplementaryEbook": "אין ספר אלקטרוני נלווה", "LabelMobileRedirectURIs": "כתובות משדר ניידות מורשות", "LabelMobileRedirectURIsDescription": "זהו רשימה לבניה של כתובות ה-URI הנתמכות להפניות עבור אפליקציות ניידות. הברירת מחדל היא <code>audiobookshelf://oauth</code>, שניתן להסיר או להוסיף לה כתובות נוספות לאינטגרציה עם אפליקציות צד שלישי. שימוש בכוכבית (<code>*</code>) כקלט בודד מאפשר כל URI.", diff --git a/client/strings/hi.json b/client/strings/hi.json index 4e8aae7b..3859f3d7 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Every 6 hours", "LabelIntervalEveryDay": "Every day", "LabelIntervalEveryHour": "Every hour", - "LabelInvalidParts": "Invalid Parts", "LabelInvert": "Invert", "LabelItem": "Item", "LabelLanguage": "Language", @@ -357,7 +356,6 @@ "LabelMinute": "Minute", "LabelMissing": "Missing", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Missing Parts", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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.", diff --git a/client/strings/hr.json b/client/strings/hr.json index fa5bcd35..3c81c88d 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Every 6 hours", "LabelIntervalEveryDay": "Every day", "LabelIntervalEveryHour": "Every hour", - "LabelInvalidParts": "Nevaljajuči dijelovi", "LabelInvert": "Invert", "LabelItem": "Stavka", "LabelLanguage": "Jezik", @@ -357,7 +356,6 @@ "LabelMinute": "Minuta", "LabelMissing": "Nedostaje", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Nedostajali dijelovi", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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.", diff --git a/client/strings/hu.json b/client/strings/hu.json index f7e2abd8..c5eec3c7 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Minden 6 órában", "LabelIntervalEveryDay": "Minden nap", "LabelIntervalEveryHour": "Minden órában", - "LabelInvalidParts": "Érvénytelen részek", "LabelInvert": "Megfordítás", "LabelItem": "Elem", "LabelLanguage": "Nyelv", @@ -357,7 +356,6 @@ "LabelMinute": "Perc", "LabelMissing": "Hiányzó", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Hiányzó részek", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Engedélyezett mobil átirányítási URI-k", "LabelMobileRedirectURIsDescription": "Ez egy fehérlista az érvényes mobilalkalmazás-átirányítási URI-k számára. Az alapértelmezett <code>audiobookshelf://oauth</code>, amely eltávolítható vagy kiegészíthető további URI-kkal harmadik féltől származó alkalmazásintegráció érdekében. Ha az egyetlen bejegyzés egy csillag (<code>*</code>), akkor bármely URI engedélyezett.", diff --git a/client/strings/it.json b/client/strings/it.json index bd6e5fa9..22c0503a 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Ogni 6 ore", "LabelIntervalEveryDay": "Ogni Giorno", "LabelIntervalEveryHour": "Ogni ora", - "LabelInvalidParts": "Parti Invalide", "LabelInvert": "Inverti", "LabelItem": "Oggetti", "LabelLanguage": "Lingua", @@ -357,7 +356,6 @@ "LabelMinute": "Minuto", "LabelMissing": "Altro", "LabelMissingEbook": "Non ha ebook", - "LabelMissingParts": "Parti rimanenti", "LabelMissingSupplementaryEbook": "Non ha ebook supplementare", "LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti", "LabelMobileRedirectURIsDescription": "Questa è una lista bianca di URI di reindirizzamento validi per le app mobili. Quello predefinito è <code>audiobookshelf://oauth</code>, che puoi rimuovere o integrare con URI aggiuntivi per l'integrazione di app di terze parti. Utilizzando un asterisco (<code>*</code>) poiché l'unica voce consente qualsiasi URI.", @@ -778,4 +776,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 d36861a4..3eaba010 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Kas 6 valandas", "LabelIntervalEveryDay": "Kasdien", "LabelIntervalEveryHour": "Kiekvieną valandą", - "LabelInvalidParts": "Netinkamos dalys", "LabelInvert": "Apversti", "LabelItem": "Elementas", "LabelLanguage": "Kalba", @@ -357,7 +356,6 @@ "LabelMinute": "Minutė", "LabelMissing": "Trūksta", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Trūkstamos dalys", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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.", diff --git a/client/strings/nl.json b/client/strings/nl.json index b8d3f669..fb746672 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Iedere 6 uur", "LabelIntervalEveryDay": "Iedere dag", "LabelIntervalEveryHour": "Ieder uur", - "LabelInvalidParts": "Ongeldige delen", "LabelInvert": "Omdraaien", "LabelItem": "Onderdeel", "LabelLanguage": "Taal", @@ -357,7 +356,6 @@ "LabelMinute": "Minuut", "LabelMissing": "Ontbrekend", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Ontbrekende delen", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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.", diff --git a/client/strings/no.json b/client/strings/no.json index 818ee0fa..b66ab6f3 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Hver 6. timer", "LabelIntervalEveryDay": "Hver dag", "LabelIntervalEveryHour": "Hver time", - "LabelInvalidParts": "Ugyldige deler", "LabelInvert": "Inverter", "LabelItem": "Enhet", "LabelLanguage": "Språk", @@ -357,7 +356,6 @@ "LabelMinute": "Minutt", "LabelMissing": "Mangler", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Manglende deler", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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.", diff --git a/client/strings/pl.json b/client/strings/pl.json index cf4274cc..e1d7f289 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Co 6 godzin", "LabelIntervalEveryDay": "Każdego dnia", "LabelIntervalEveryHour": "Każdej godziny", - "LabelInvalidParts": "Nieprawidłowe części", "LabelInvert": "Invert", "LabelItem": "Pozycja", "LabelLanguage": "Język", @@ -357,7 +356,6 @@ "LabelMinute": "Minuta", "LabelMissing": "Brakujący", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Brakujące cześci", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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.", diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index c4d00eb7..47edcfac 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "A cada 6 horas", "LabelIntervalEveryDay": "Todo dia", "LabelIntervalEveryHour": "Toda hora", - "LabelInvalidParts": "Partes Inválidas", "LabelInvert": "Inverter", "LabelItem": "Item", "LabelLanguage": "Idioma", @@ -357,7 +356,6 @@ "LabelMinute": "Minuto", "LabelMissing": "Ausente", "LabelMissingEbook": "Ebook não existe", - "LabelMissingParts": "Partes Ausentes", "LabelMissingSupplementaryEbook": "Ebook complementar não existe", "LabelMobileRedirectURIs": "URIs de redirecionamento móveis permitidas", "LabelMobileRedirectURIsDescription": "Essa é uma lista de permissionamento para URIs válidas para o redirecionamento de aplicativos móveis. A padrão é <code>audiobookshelf://oauth</code>, que pode ser removida ou acrescentada com novas URIs para integração com apps de terceiros. Usando um asterisco (<code>*</code>) como um item único dará permissão para qualquer URI.", @@ -778,4 +776,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} +} \ No newline at end of file diff --git a/client/strings/ru.json b/client/strings/ru.json index a55e4668..9388739d 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Каждые 6 часов", "LabelIntervalEveryDay": "Каждый день", "LabelIntervalEveryHour": "Каждый час", - "LabelInvalidParts": "Неверные части", "LabelInvert": "Инвертировать", "LabelItem": "Элемент", "LabelLanguage": "Язык", @@ -357,7 +356,6 @@ "LabelMinute": "Минуты", "LabelMissing": "Потеряно", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Потерянные части", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств", "LabelMobileRedirectURIsDescription": "Это белый список допустимых URI перенаправления для мобильных приложений. По умолчанию используется <code>audiobookshelf://oauth</code>, который можно удалить или дополнить дополнительными URI для интеграции со сторонними приложениями. Использование звездочки (<code>*</code>) в качестве единственной записи разрешает любой URI.", diff --git a/client/strings/sv.json b/client/strings/sv.json index 820fe18a..b16f6aec 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Var 6:e timme", "LabelIntervalEveryDay": "Varje dag", "LabelIntervalEveryHour": "Varje timme", - "LabelInvalidParts": "Ogiltiga delar", "LabelInvert": "Invertera", "LabelItem": "Objekt", "LabelLanguage": "Språk", @@ -357,7 +356,6 @@ "LabelMinute": "Minut", "LabelMissing": "Saknad", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Saknade delar", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "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.", diff --git a/client/strings/uk.json b/client/strings/uk.json index 9953bda5..7c0d4476 100644 --- a/client/strings/uk.json +++ b/client/strings/uk.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Кожні 6 годин", "LabelIntervalEveryDay": "Щодня", "LabelIntervalEveryHour": "Щогодини", - "LabelInvalidParts": "Недопустимі частини", "LabelInvert": "Інвертувати", "LabelItem": "Елемент", "LabelLanguage": "Мова", @@ -357,7 +356,6 @@ "LabelMinute": "Хвилина", "LabelMissing": "Бракує", "LabelMissingEbook": "Без електронної книги", - "LabelMissingParts": "Відсутні частини", "LabelMissingSupplementaryEbook": "Без додаткової електронної книги", "LabelMobileRedirectURIs": "Дозволені адреси перенаправлення", "LabelMobileRedirectURIsDescription": "Це білий список наявних URI, що перенаправляють у мобільний додаток. За замовчуванням це <code>audiobookshelf://oauth</code>, який ви можете видалити або ж додати інші адреси для сторонніх інтеграцій. Використайте зірочку (<code>*</code>), аби дозволити будь-яке URI.", @@ -778,4 +776,4 @@ "ToastSocketFailedToConnect": "Не вдалося під'єднатися до сокета", "ToastUserDeleteFailed": "Не вдалося видалити користувача", "ToastUserDeleteSuccess": "Користувача видалено" -} +} \ No newline at end of file diff --git a/client/strings/vi-vn.json b/client/strings/vi-vn.json index bddfd647..1f356676 100644 --- a/client/strings/vi-vn.json +++ b/client/strings/vi-vn.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Mỗi 6 giờ", "LabelIntervalEveryDay": "Mỗi ngày", "LabelIntervalEveryHour": "Mỗi giờ", - "LabelInvalidParts": "Phần không hợp lệ", "LabelInvert": "Nghịch đảo", "LabelItem": "Mục", "LabelLanguage": "Ngôn ngữ", @@ -357,7 +356,6 @@ "LabelMinute": "Phút", "LabelMissing": "Thiếu", "LabelMissingEbook": "Không có ebook", - "LabelMissingParts": "Các phần thiếu", "LabelMissingSupplementaryEbook": "Không có ebook bổ sung", "LabelMobileRedirectURIs": "URI chuyển hướng di động được cho phép", "LabelMobileRedirectURIsDescription": "Đây là danh sách trắng các URI chuyển hướng hợp lệ cho ứng dụng di động. Mặc định là <code>audiobookshelf://oauth</code>, bạn có thể loại bỏ hoặc bổ sung thêm các URI cho tích hợp ứng dụng bên thứ ba. Sử dụng dấu hoa thị (<code>*</code>) như một mục duy nhất cho phép bất kỳ URI nào.", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index bf816f89..2dc1ab16 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "每 6 小时", "LabelIntervalEveryDay": "每天", "LabelIntervalEveryHour": "每小时", - "LabelInvalidParts": "无效部件", "LabelInvert": "倒转", "LabelItem": "项目", "LabelLanguage": "语言", @@ -357,7 +356,6 @@ "LabelMinute": "分钟", "LabelMissing": "丢失", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "丢失的部分", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "允许移动应用重定向 URI", "LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 <code>audiobookshelf://oauth</code>,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (<code>*</code>) 作为唯一条目允许任何 URI.", diff --git a/client/strings/zh-tw.json b/client/strings/zh-tw.json index 5a9afc98..23321911 100644 --- a/client/strings/zh-tw.json +++ b/client/strings/zh-tw.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "每 6 小時", "LabelIntervalEveryDay": "每天", "LabelIntervalEveryHour": "每小時", - "LabelInvalidParts": "無效部件", "LabelInvert": "倒轉", "LabelItem": "項目", "LabelLanguage": "語言", @@ -357,7 +356,6 @@ "LabelMinute": "分鐘", "LabelMissing": "丟失", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "丟失的部分", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "允許移動應用重定向 URI", "LabelMobileRedirectURIsDescription": "這是移動應用程序的有效重定向 URI 白名單. 預設值為 <code>audiobookshelf://oauth</code>,您可以刪除它或加入其他 URI 以進行第三方應用集成. 使用星號 (<code>*</code>) 作為唯一條目允許任何 URI.", @@ -466,6 +464,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "只有一本書的系列將從系列頁面和主頁書架中隱藏.", "LabelSettingsHomePageBookshelfView": "首頁使用書架視圖", "LabelSettingsLibraryBookshelfView": "媒體庫使用書架視圖", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "解析副標題", "LabelSettingsParseSubtitlesHelp": "從有聲書資料夾中提取副標題.<br>副標題必須用 \" - \" 分隔.<br>例: \"書名 - 這裡是副標題\" 則顯示副標題 \"這裡是副標題\"", "LabelSettingsPreferMatchedMetadata": "首選匹配的元數據", @@ -776,4 +776,4 @@ "ToastSocketFailedToConnect": "網路連接失敗", "ToastUserDeleteFailed": "刪除使用者失敗", "ToastUserDeleteSuccess": "使用者已刪除" -} +} \ No newline at end of file diff --git a/server/models/Book.js b/server/models/Book.js index 6b179c36..e2b56fbe 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -7,7 +7,7 @@ const Logger = require('../Logger') * @property {string} ebookFormat * @property {number} addedAt * @property {number} updatedAt - * @property {{filename:string, ext:string, path:string, relPath:string, size:number, mtimeMs:number, ctimeMs:number, birthtimeMs:number}} metadata + * @property {{filename:string, ext:string, path:string, relPath:strFing, size:number, mtimeMs:number, ctimeMs:number, birthtimeMs:number}} metadata */ /** diff --git a/server/objects/files/AudioFile.js b/server/objects/files/AudioFile.js index 8a3c2a74..c0c425ba 100644 --- a/server/objects/files/AudioFile.js +++ b/server/objects/files/AudioFile.js @@ -32,7 +32,6 @@ class AudioFile { this.metaTags = null this.manuallyVerified = false - this.invalid = false this.exclude = false this.error = null @@ -53,7 +52,6 @@ class AudioFile { trackNumFromFilename: this.trackNumFromFilename, discNumFromFilename: this.discNumFromFilename, manuallyVerified: !!this.manuallyVerified, - invalid: !!this.invalid, exclude: !!this.exclude, error: this.error || null, format: this.format, @@ -78,7 +76,6 @@ class AudioFile { this.addedAt = data.addedAt this.updatedAt = data.updatedAt this.manuallyVerified = !!data.manuallyVerified - this.invalid = !!data.invalid this.exclude = !!data.exclude this.error = data.error || null @@ -112,10 +109,6 @@ class AudioFile { } } - get isValidTrack() { - return !this.invalid && !this.exclude - } - // New scanner creates AudioFile from AudioFileScanner setDataFromProbe(libraryFile, probeData) { this.ino = libraryFile.ino || null diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js index d53a53a7..8fdff988 100644 --- a/server/objects/mediaTypes/Book.js +++ b/server/objects/mediaTypes/Book.js @@ -17,7 +17,6 @@ class Book { this.audioFiles = [] this.chapters = [] - this.missingParts = [] this.ebookFile = null this.lastCoverSearch = null @@ -36,7 +35,6 @@ class Book { this.tags = [...book.tags] this.audioFiles = book.audioFiles.map(f => new AudioFile(f)) this.chapters = book.chapters.map(c => ({ ...c })) - this.missingParts = book.missingParts ? [...book.missingParts] : [] this.ebookFile = book.ebookFile ? new EBookFile(book.ebookFile) : null this.lastCoverSearch = book.lastCoverSearch || null this.lastCoverSearchQuery = book.lastCoverSearchQuery || null @@ -51,7 +49,6 @@ class Book { tags: [...this.tags], audioFiles: this.audioFiles.map(f => f.toJSON()), chapters: this.chapters.map(c => ({ ...c })), - missingParts: [...this.missingParts], ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null } } @@ -65,8 +62,6 @@ class Book { numTracks: this.tracks.length, numAudioFiles: this.audioFiles.length, numChapters: this.chapters.length, - numMissingParts: this.missingParts.length, - numInvalidAudioFiles: this.invalidAudioFiles.length, duration: this.duration, size: this.size, ebookFormat: this.ebookFile?.ebookFormat @@ -85,7 +80,6 @@ class Book { duration: this.duration, size: this.size, tracks: this.tracks.map(t => t.toJSON()), - missingParts: [...this.missingParts], ebookFile: this.ebookFile?.toJSON() || null } } @@ -109,11 +103,8 @@ class Book { get hasMediaEntities() { return !!this.tracks.length || this.ebookFile } - get invalidAudioFiles() { - return this.audioFiles.filter(af => af.invalid) - } get includedAudioFiles() { - return this.audioFiles.filter(af => !af.exclude && !af.invalid) + return this.audioFiles.filter(af => !af.exclude) } get tracks() { let startOffset = 0 @@ -238,7 +229,6 @@ class Book { this.audioFiles = orderedFileData.map((fileData) => { const audioFile = this.audioFiles.find(af => af.ino === fileData.ino) audioFile.manuallyVerified = true - audioFile.invalid = false audioFile.error = null if (fileData.exclude !== undefined) { audioFile.exclude = !!fileData.exclude @@ -257,7 +247,6 @@ class Book { rebuildTracks() { Logger.debug(`[Book] Tracks being rebuilt...!`) this.audioFiles.sort((a, b) => a.index - b.index) - this.missingParts = [] } // Only checks container format From 7d0eb215d6d0d5e39fb4480daf3e765104142316 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Fri, 22 Mar 2024 01:28:50 +0000 Subject: [PATCH 0474/2145] Add integration workflow --- .github/workflows/i18n-integration.yml | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/i18n-integration.yml diff --git a/.github/workflows/i18n-integration.yml b/.github/workflows/i18n-integration.yml new file mode 100644 index 00000000..cee07b20 --- /dev/null +++ b/.github/workflows/i18n-integration.yml @@ -0,0 +1,27 @@ +name: Verify all i18n files are alphabetized + +on: + push: + paths: + - client/strings/** # Should only check if any strings changed + +jobs: + update_translations: + runs-on: ubuntu-latest + steps: + # Check out the repository + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up node to run the javascript + - name: Set up node + uses: actions/setup-node@v4 + with: + node-version: "20" + + # The only argument is the `directory`, which is where the i18n files are + # stored. + - name: Run Update JSON Files action + uses: audiobookshelf/audiobookshelf-i18n-updater@v1.1.1 + with: + directory: "client/strings/" # Adjust the directory path as needed From 961533765f8006492c3c1ee8c877ab29c255e32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rasmus=20Kr=C3=A4mer?= <git@rfk.io> Date: Sat, 23 Mar 2024 14:54:34 +0100 Subject: [PATCH 0475/2145] Fix custom metadata provider crash --- server/finders/BookFinder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 2c6fc9ee..9aa0a182 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -158,7 +158,7 @@ class BookFinder { * @returns {Promise<Object[]>} */ async getCustomProviderResults(title, author, isbn, providerSlug) { - const books = await this.customProviderAdapter.search(title, author, providerSlug, 'book') + const books = await this.customProviderAdapter.search(title, author, isbn, providerSlug, 'book') if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`) return books From 68276fe30ba4397e1d293e855e8740f4efb93453 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Sat, 23 Mar 2024 18:31:52 +0200 Subject: [PATCH 0476/2145] Fix handling of file moves from root folder to sub folder and back --- server/scanner/LibraryItemScanner.js | 2 +- server/scanner/LibraryScanner.js | 75 +++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/server/scanner/LibraryItemScanner.js b/server/scanner/LibraryItemScanner.js index 872000d8..1c3123df 100644 --- a/server/scanner/LibraryItemScanner.js +++ b/server/scanner/LibraryItemScanner.js @@ -56,7 +56,7 @@ class LibraryItemScanner { const libraryItemPath = updateLibraryItemDetails?.path || fileUtils.filePathToPOSIX(libraryItem.path) const folder = library.libraryFolders[0] - const libraryItemScanData = await this.getLibraryItemScanData(libraryItemPath, library, folder, false) + const libraryItemScanData = await this.getLibraryItemScanData(libraryItemPath, library, folder, updateLibraryItemDetails?.isFile || false) let libraryItemDataUpdated = await libraryItemScanData.checkLibraryItemData(libraryItem, scanLogger) diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index ac422c79..d394128c 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -154,7 +154,11 @@ class LibraryScanner { let libraryItemData = libraryItemDataFound.find(lid => lid.path === existingLibraryItem.path) if (!libraryItemData) { // Fallback to finding matching library item with matching inode value - libraryItemData = libraryItemDataFound.find(lid => lid.ino === existingLibraryItem.ino) + libraryItemData = libraryItemDataFound.find(lid => + ItemToItemInoMatch(lid, existingLibraryItem) || + ItemToFileInoMatch(lid, existingLibraryItem) || + ItemToFileInoMatch(existingLibraryItem, lid) + ) if (libraryItemData) { libraryScan.addLog(LogLevel.INFO, `Library item with path "${existingLibraryItem.path}" was not found, but library item inode "${existingLibraryItem.ino}" was found at path "${libraryItemData.path}"`) } @@ -522,23 +526,25 @@ class LibraryScanner { // Check if book dir group is already an item let existingLibraryItem = await Database.libraryItemModel.findOneOld({ + libraryId: library.id, path: potentialChildDirs }) let updatedLibraryItemDetails = {} if (!existingLibraryItem) { - const dirIno = await fileUtils.getIno(fullPath) - existingLibraryItem = await Database.libraryItemModel.findOneOld({ - ino: dirIno - }) + const isSingleMedia = isSingleMediaFile(fileUpdateGroup, itemDir) + existingLibraryItem = + await findLibraryItemByItemToItemInoMatch(library.id, fullPath) || + await findLibraryItemByItemToFileInoMatch(library.id, fullPath, isSingleMedia) || + await findLibraryItemByFileToItemInoMatch(library.id, fullPath, isSingleMedia, fileUpdateGroup[itemDir]) if (existingLibraryItem) { - Logger.debug(`[LibraryScanner] scanFolderUpdates: Library item found by inode value=${dirIno}. "${existingLibraryItem.relPath} => ${itemDir}"`) // Update library item paths for scan existingLibraryItem.path = fullPath existingLibraryItem.relPath = itemDir updatedLibraryItemDetails.path = fullPath updatedLibraryItemDetails.relPath = itemDir updatedLibraryItemDetails.libraryFolderId = folder.id + updatedLibraryItemDetails.isFile = isSingleMedia } } if (existingLibraryItem) { @@ -555,7 +561,6 @@ class LibraryScanner { continue } } - // 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, updatedLibraryItemDetails) @@ -595,6 +600,14 @@ class LibraryScanner { } module.exports = new LibraryScanner() +function ItemToFileInoMatch(libraryItem1, libraryItem2) { + return libraryItem1.isFile && libraryItem2.libraryFiles.some(lf => lf.ino === libraryItem1.ino) +} + +function ItemToItemInoMatch(libraryItem1, libraryItem2) { + return libraryItem1.ino === libraryItem2.ino +} + function hasAudioFiles(fileUpdateGroup, itemDir) { return isSingleMediaFile(fileUpdateGroup, itemDir) ? scanUtils.checkFilepathIsAudioFile(fileUpdateGroup[itemDir]) : @@ -604,3 +617,51 @@ function hasAudioFiles(fileUpdateGroup, itemDir) { function isSingleMediaFile(fileUpdateGroup, itemDir) { return itemDir === fileUpdateGroup[itemDir] } + +async function findLibraryItemByItemToItemInoMatch(libraryId, fullPath) { + const ino = await fileUtils.getIno(fullPath) + if (!ino) return null + const existingLibraryItem = await Database.libraryItemModel.findOneOld({ + libraryId: libraryId, + ino: ino + }) + if (existingLibraryItem) + Logger.debug(`[LibraryScanner] Found library item with matching inode "${ino}" at path "${existingLibraryItem.path}"`) + return existingLibraryItem +} + +async function findLibraryItemByItemToFileInoMatch(libraryId, fullPath, isSingleMedia) { + if (!isSingleMedia) return null + // check if it was moved from another folder by comparing the ino to the library files + const ino = await fileUtils.getIno(fullPath) + if (!ino) return null + const existingLibraryItem = await Database.libraryItemModel.findOneOld({ + libraryId: libraryId, + libraryFiles: { + [ sequelize.Op.substring ]: ino + } + }) + if (existingLibraryItem) + Logger.debug(`[LibraryScanner] Found library item with a library file matching inode "${ino}" at path "${existingLibraryItem.path}"`) + return existingLibraryItem +} + +async function findLibraryItemByFileToItemInoMatch(libraryId, fullPath, isSingleMedia, itemFiles) { + if (isSingleMedia) return null + // check if it was moved from the root folder by comparing the ino to the ino of the scanned files + let itemFileInos = [] + for (const itemFile of itemFiles) { + const ino = await fileUtils.getIno(Path.posix.join(fullPath, itemFile)) + if (ino) itemFileInos.push(ino) + } + if (!itemFileInos.length) return null + const existingLibraryItem = await Database.libraryItemModel.findOneOld({ + libraryId: libraryId, + ino: { + [ sequelize.Op.in ] : itemFileInos + } + }) + if (existingLibraryItem) + Logger.debug(`[LibraryScanner] Found library item with inode matching one of "${itemFileInos.join(',')}" at path "${existingLibraryItem.path}"`) + return existingLibraryItem +} \ No newline at end of file From f827aa97f8d74aead118387f2e8cd608cad26393 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 23 Mar 2024 14:56:32 -0500 Subject: [PATCH 0477/2145] Update library scanner findLibraryItemByItemToFileInoMatch query to iterate through json objects comparing inodes --- server/Logger.js | 2 +- server/models/LibraryItem.js | 10 ++++++---- server/scanner/LibraryScanner.js | 28 ++++++++++++++++------------ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/server/Logger.js b/server/Logger.js index 7cc7aa4c..8283e1f0 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -93,7 +93,7 @@ class Logger { // Save log to file if (level >= this.logLevel) { - await this.logManager.logToFile(logObj) + await this.logManager?.logToFile(logObj) } } diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 44758750..704e2f10 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -1,4 +1,4 @@ -const { DataTypes, Model, WhereOptions } = require('sequelize') +const { DataTypes, Model } = require('sequelize') const Logger = require('../Logger') const oldLibraryItem = require('../objects/LibraryItem') const libraryFilters = require('../utils/queries/libraryFilters') @@ -116,7 +116,7 @@ class LibraryItem extends Model { /** * Currently unused because this is too slow and uses too much mem - * @param {[WhereOptions]} where + * @param {import('sequelize').WhereOptions} [where] * @returns {Array<objects.LibraryItem>} old library items */ static async getAllOldLibraryItems(where = null) { @@ -773,12 +773,14 @@ class LibraryItem extends Model { /** * - * @param {WhereOptions} where + * @param {import('sequelize').WhereOptions} where + * @param {import('sequelize').BindOrReplacements} replacements * @returns {Object} oldLibraryItem */ - static async findOneOld(where) { + static async findOneOld(where, replacements = {}) { const libraryItem = await this.findOne({ where, + replacements, include: [ { model: this.sequelize.models.book, diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index d394128c..8131300d 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -155,10 +155,10 @@ class LibraryScanner { if (!libraryItemData) { // Fallback to finding matching library item with matching inode value libraryItemData = libraryItemDataFound.find(lid => - ItemToItemInoMatch(lid, existingLibraryItem) || - ItemToFileInoMatch(lid, existingLibraryItem) || + ItemToItemInoMatch(lid, existingLibraryItem) || + ItemToFileInoMatch(lid, existingLibraryItem) || ItemToFileInoMatch(existingLibraryItem, lid) - ) + ) if (libraryItemData) { libraryScan.addLog(LogLevel.INFO, `Library item with path "${existingLibraryItem.path}" was not found, but library item inode "${existingLibraryItem.ino}" was found at path "${libraryItemData.path}"`) } @@ -533,8 +533,8 @@ class LibraryScanner { let updatedLibraryItemDetails = {} if (!existingLibraryItem) { const isSingleMedia = isSingleMediaFile(fileUpdateGroup, itemDir) - existingLibraryItem = - await findLibraryItemByItemToItemInoMatch(library.id, fullPath) || + existingLibraryItem = + await findLibraryItemByItemToItemInoMatch(library.id, fullPath) || await findLibraryItemByItemToFileInoMatch(library.id, fullPath, isSingleMedia) || await findLibraryItemByFileToItemInoMatch(library.id, fullPath, isSingleMedia, fileUpdateGroup[itemDir]) if (existingLibraryItem) { @@ -630,16 +630,20 @@ async function findLibraryItemByItemToItemInoMatch(libraryId, fullPath) { return existingLibraryItem } -async function findLibraryItemByItemToFileInoMatch(libraryId, fullPath, isSingleMedia) { +async function findLibraryItemByItemToFileInoMatch(libraryId, fullPath, isSingleMedia) { if (!isSingleMedia) return null // check if it was moved from another folder by comparing the ino to the library files const ino = await fileUtils.getIno(fullPath) if (!ino) return null - const existingLibraryItem = await Database.libraryItemModel.findOneOld({ - libraryId: libraryId, - libraryFiles: { - [ sequelize.Op.substring ]: ino - } + const existingLibraryItem = await Database.libraryItemModel.findOneOld([ + { + libraryId: libraryId + }, + sequelize.where(sequelize.literal('(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(json_each.value) AND json_each.value->>"$.ino" = :inode)'), { + [sequelize.Op.gt]: 0 + }) + ], { + inode: ino }) if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with a library file matching inode "${ino}" at path "${existingLibraryItem.path}"`) @@ -658,7 +662,7 @@ async function findLibraryItemByFileToItemInoMatch(libraryId, fullPath, isSingle const existingLibraryItem = await Database.libraryItemModel.findOneOld({ libraryId: libraryId, ino: { - [ sequelize.Op.in ] : itemFileInos + [sequelize.Op.in]: itemFileInos } }) if (existingLibraryItem) From 125346bb5c9b2b879d686a3a0d9e80a9b46afb8b Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Sun, 24 Mar 2024 03:11:00 +0000 Subject: [PATCH 0478/2145] Translation guide link added to readme --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 1f3f1d05..1fc2e327 100644 --- a/readme.md +++ b/readme.md @@ -336,6 +336,8 @@ Need to do one of following: This application is built using [NodeJs](https://nodejs.org/). +Information on helping with translations of the web client [here](https://www.audiobookshelf.org/faq#how-do-i-help-with-translations). + ### Dev Container Setup The easiest way to begin developing this project is to use a dev container. An introduction to dev containers in VSCode can be found [here](https://code.visualstudio.com/docs/devcontainers/containers). From 2d68fa2c278d5c3b5642486c8d410935a2baf3af Mon Sep 17 00:00:00 2001 From: Lauri Vuorela <lauri.vuorela@gmail.com> Date: Mon, 25 Mar 2024 16:32:29 +0100 Subject: [PATCH 0479/2145] fix book limit for the contiue series shelf --- server/utils/queries/libraryItemsBookFilters.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 07a8458d..12bc9701 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -658,8 +658,13 @@ module.exports = { let includeAttributes = [ [Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress'], ] + let booksNotFinishedQuery = `SELECT count(*) FROM bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = bs.bookId AND mp.userId = :userId WHERE bs.seriesId = series.id AND (mp.isFinished = 0 OR mp.isFinished IS NULL)` + if (library.settings.onlyShowLaterBooksInContinueSeries) { - includeAttributes.push([Sequelize.literal('(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.isFinished = 1 AND mp.userId = :userId AND bs.seriesId = series.id)'), 'maxSequence']) + const maxSequenceQuery = `(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.isFinished = 1 AND mp.userId = :userId AND bs.seriesId = series.id)` + includeAttributes.push([Sequelize.literal(`${maxSequenceQuery}`), 'maxSequence']) + + booksNotFinishedQuery = booksNotFinishedQuery + ` AND CAST(bs.sequence as FLOAT) > ${maxSequenceQuery}` } const { rows: series, count } = await Database.seriesModel.findAndCountAll({ @@ -675,8 +680,8 @@ module.exports = { Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM mediaProgresses mp, bookSeries bs WHERE bs.seriesId = series.id AND mp.mediaItemId = bs.bookId AND mp.userId = :userId AND mp.isFinished = 1)`), { [Sequelize.Op.gte]: 1 }), - // Has at least 1 book not finished - Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = bs.bookId AND mp.userId = :userId WHERE bs.seriesId = series.id AND (mp.isFinished = 0 OR mp.isFinished IS NULL))`), { + // Has at least 1 book not finished (that has a sequence number higher than the highest already read, if library config is toggled) + Sequelize.where(Sequelize.literal(`(${booksNotFinishedQuery})`), { [Sequelize.Op.gte]: 1 }), // Has no books in progress From 8ce5a5cdbdbcda02c65fb0dbd1e6a25c2d3b6fe8 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 27 Mar 2024 13:18:02 +0200 Subject: [PATCH 0480/2145] Add workflow to dispatch an abs-windows event --- .github/workflows/notify-abs-windows.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/notify-abs-windows.yml diff --git a/.github/workflows/notify-abs-windows.yml b/.github/workflows/notify-abs-windows.yml new file mode 100644 index 00000000..9ede33b8 --- /dev/null +++ b/.github/workflows/notify-abs-windows.yml @@ -0,0 +1,17 @@ +name: Dispatch an abs-windows event + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + abs-windows-dispatch: + runs-on: ubuntu-latest + steps: + - name: Send a remote repository dispatch event + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.ABS_WINDOWS_PAT }} + repository: mikiher/audiobookshelf-windows + event-type: build-windows From 1cf0bd0f01d442899fc5943dfbdfde6704a76c74 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 27 Mar 2024 13:30:00 +0200 Subject: [PATCH 0481/2145] add dummy pull_request event for the workflow to appear in the list --- .github/workflows/notify-abs-windows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/notify-abs-windows.yml b/.github/workflows/notify-abs-windows.yml index 9ede33b8..2f7414c8 100644 --- a/.github/workflows/notify-abs-windows.yml +++ b/.github/workflows/notify-abs-windows.yml @@ -4,6 +4,8 @@ on: release: types: [published] workflow_dispatch: + pull_request: + types: [opened] jobs: abs-windows-dispatch: From 33e4b51aee873b9a00ae54bb95c36efdcae7c842 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Wed, 27 Mar 2024 13:38:17 +0200 Subject: [PATCH 0482/2145] Revert "add dummy pull_request event for the workflow to appear in the list" This reverts commit 1cf0bd0f01d442899fc5943dfbdfde6704a76c74. --- .github/workflows/notify-abs-windows.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/notify-abs-windows.yml b/.github/workflows/notify-abs-windows.yml index 2f7414c8..9ede33b8 100644 --- a/.github/workflows/notify-abs-windows.yml +++ b/.github/workflows/notify-abs-windows.yml @@ -4,8 +4,6 @@ on: release: types: [published] workflow_dispatch: - pull_request: - types: [opened] jobs: abs-windows-dispatch: From 740640884fa8ec2f85acc20d48d7aeb65d66e967 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 27 Mar 2024 16:11:47 -0500 Subject: [PATCH 0483/2145] Update:Support for comic files with webp images #2792 --- client/components/readers/ComicReader.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/readers/ComicReader.vue b/client/components/readers/ComicReader.vue index d55fc0d6..49e9b093 100644 --- a/client/components/readers/ComicReader.vue +++ b/client/components/readers/ComicReader.vue @@ -334,7 +334,7 @@ export default { } }, parseFilenames(filenames) { - const acceptableImages = ['.jpeg', '.jpg', '.png'] + const acceptableImages = ['.jpeg', '.jpg', '.png', '.webp'] var imageFiles = filenames.filter((f) => { return acceptableImages.includes((Path.extname(f) || '').toLowerCase()) }) From 617b8f4487d506da962658a8fd371584e8ba7734 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Thu, 28 Mar 2024 16:16:26 +0100 Subject: [PATCH 0484/2145] OpenID: Rename tags switch --- server/objects/user/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/objects/user/User.js b/server/objects/user/User.js index d09e921d..b473637b 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -277,7 +277,7 @@ class User { canAccessExplicitContent: 'accessExplicitContent', canAccessAllLibraries: 'accessAllLibraries', canAccessAllTags: 'accessAllTags', - tagsAreBlacklist: 'selectedTagsNotAccessible', + tagsAreDenylist: 'selectedTagsNotAccessible', // Direct mapping for array-based permissions allowedLibraries: 'librariesAccessible', allowedTags: 'itemTagsSelected', From 33254654d5fbf589eee90861309a5ef2a51d572b Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Thu, 28 Mar 2024 23:56:59 +0200 Subject: [PATCH 0485/2145] Add dir="auto" attribute where it makes sense --- client/components/cards/LazyBookCard.vue | 2 +- client/components/modals/item/tabs/Episodes.vue | 2 +- client/components/modals/podcast/ViewEpisode.vue | 4 ++-- client/components/tables/ChaptersTable.vue | 2 +- client/components/tables/podcast/DownloadQueueTable.vue | 2 +- client/components/tables/podcast/LazyEpisodeRow.vue | 2 +- client/components/ui/TextInput.vue | 2 +- client/components/ui/TextareaInput.vue | 2 +- client/pages/item/_id/index.vue | 2 +- client/pages/library/_library/podcast/latest.vue | 4 ++-- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index efeb0165..faa93997 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -6,7 +6,7 @@ </div> <!-- Alternative bookshelf title/author/sort --> - <div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }"> + <div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }"> <div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }"> <ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center"> <p ref="displayTitle" class="truncate">{{ displayTitle }}</p> diff --git a/client/components/modals/item/tabs/Episodes.vue b/client/components/modals/item/tabs/Episodes.vue index 661f41e0..ecf58330 100644 --- a/client/components/modals/item/tabs/Episodes.vue +++ b/client/components/modals/item/tabs/Episodes.vue @@ -29,7 +29,7 @@ <td class="text-center w-20 min-w-20"> <p>{{ episode.episode }}</p> </td> - <td> + <td dir="auto"> {{ episode.title }} </td> <td class="font-mono text-center"> diff --git a/client/components/modals/podcast/ViewEpisode.vue b/client/components/modals/podcast/ViewEpisode.vue index 79f22a03..411e9efd 100644 --- a/client/components/modals/podcast/ViewEpisode.vue +++ b/client/components/modals/podcast/ViewEpisode.vue @@ -15,8 +15,8 @@ <p class="text-xs text-gray-300">{{ podcastAuthor }}</p> </div> </div> - <p class="text-lg font-semibold mb-6">{{ title }}</p> - <div v-if="description" class="default-style" v-html="description" /> + <p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p> + <div v-if="description" dir="auto" class="default-style" v-html="description" /> <p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p> </div> </modals-modal> diff --git a/client/components/tables/ChaptersTable.vue b/client/components/tables/ChaptersTable.vue index 0dd9f2ab..2abe1607 100644 --- a/client/components/tables/ChaptersTable.vue +++ b/client/components/tables/ChaptersTable.vue @@ -21,7 +21,7 @@ <td class="text-left"> <p class="px-4">{{ chapter.id }}</p> </td> - <td> + <td dir="auto"> {{ chapter.title }} </td> <td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)"> diff --git a/client/components/tables/podcast/DownloadQueueTable.vue b/client/components/tables/podcast/DownloadQueueTable.vue index 4b911229..04e631e2 100644 --- a/client/components/tables/podcast/DownloadQueueTable.vue +++ b/client/components/tables/podcast/DownloadQueueTable.vue @@ -30,7 +30,7 @@ <widgets-podcast-type-indicator :type="downloadQueued.episodeType" /> </div> </td> - <td class="px-4"> + <td dir="auto" class="px-4"> {{ downloadQueued.episodeDisplayTitle }} </td> <td class="text-xs"> diff --git a/client/components/tables/podcast/LazyEpisodeRow.vue b/client/components/tables/podcast/LazyEpisodeRow.vue index 18576340..0b32609b 100644 --- a/client/components/tables/podcast/LazyEpisodeRow.vue +++ b/client/components/tables/podcast/LazyEpisodeRow.vue @@ -2,7 +2,7 @@ <div :id="`lazy-episode-${index}`" class="w-full h-full cursor-pointer" @mouseover="mouseover" @mouseleave="mouseleave"> <div class="flex" @click="clickedEpisode"> <div class="flex-grow"> - <div class="flex items-center"> + <div dir="auto" class="flex items-center"> <span class="text-sm font-semibold">{{ episodeTitle }}</span> <widgets-podcast-type-indicator :type="episodeType" /> </div> diff --git a/client/components/ui/TextInput.vue b/client/components/ui/TextInput.vue index e06740ea..462118f0 100644 --- a/client/components/ui/TextInput.vue +++ b/client/components/ui/TextInput.vue @@ -1,6 +1,6 @@ <template> <div ref="wrapper" class="relative"> - <input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" /> + <input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" dir="auto" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" /> <div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center"> <span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span> </div> diff --git a/client/components/ui/TextareaInput.vue b/client/components/ui/TextareaInput.vue index 55c007e2..c2945707 100644 --- a/client/components/ui/TextareaInput.vue +++ b/client/components/ui/TextareaInput.vue @@ -1,5 +1,5 @@ <template> - <textarea ref="input" v-model="inputValue" :rows="rows" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @change="change" /> + <textarea ref="input" v-model="inputValue" :rows="rows" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" dir="auto" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @change="change" /> </template> <script> diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index b4b58bda..d8274e72 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -125,7 +125,7 @@ </div> <div class="my-4 w-full"> - <p ref="description" id="item-description" class="text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }">{{ description }}</p> + <p ref="description" id="item-description" dir="auto" class="text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }">{{ description }}</p> <button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription"> {{ showFullDescription ? 'Read less' : 'Read more' }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span> </button> diff --git a/client/pages/library/_library/podcast/latest.vue b/client/pages/library/_library/podcast/latest.vue index 3fc47dfd..6f1386c2 100644 --- a/client/pages/library/_library/podcast/latest.vue +++ b/client/pages/library/_library/podcast/latest.vue @@ -40,12 +40,12 @@ <div v-if="episode.episode">{{ episode.episode }}</div> </div> - <div class="flex items-center mb-2"> + <div dir="auto" class="flex items-center mb-2"> <div class="font-semibold text-sm md:text-base">{{ episode.title }}</div> <widgets-podcast-type-indicator :type="episode.episodeType" /> </div> - <p class="text-sm text-gray-200 mb-4 line-clamp-4" v-html="episode.subtitle || episode.description" /> + <p dir="auto" class="text-sm text-gray-200 mb-4 line-clamp-4" v-html="episode.subtitle || episode.description" /> <div class="flex items-center"> <button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)"> From 50bd2648aa96dd8d7746362df4acdf0e399ba35f Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 28 Mar 2024 17:00:07 -0500 Subject: [PATCH 0486/2145] Fix:Server crash on matching book with an author name ending in comma #2796 --- client/components/modals/item/tabs/Match.vue | 5 ++++- server/routers/ApiRouter.js | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 72aa7116..336a4017 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -508,7 +508,10 @@ export default { } else if (key === 'author' && !this.isPodcast) { var authors = this.selectedMatch[key] if (!Array.isArray(authors)) { - authors = authors.split(',').map((au) => au.trim()) + authors = authors + .split(',') + .map((au) => au.trim()) + .filter((au) => !!au) } var authorPayload = [] authors.forEach((authorName) => diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index d18d93e6..43e7c907 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -531,6 +531,7 @@ class ApiRouter { const authorName = (mediaMetadata.authors[i].name || '').trim() if (!authorName) { Logger.error(`[ApiRouter] Invalid author object, no name`, mediaMetadata.authors[i]) + mediaMetadata.authors[i].id = null continue } @@ -559,6 +560,8 @@ class ApiRouter { mediaMetadata.authors[i].id = author.id } } + // Remove authors without an id + mediaMetadata.authors = mediaMetadata.authors.filter(au => !!au.id) if (newAuthors.length) { await Database.createBulkAuthors(newAuthors) SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON())) @@ -572,6 +575,7 @@ class ApiRouter { const seriesName = (mediaMetadata.series[i].name || '').trim() if (!seriesName) { Logger.error(`[ApiRouter] Invalid series object, no name`, mediaMetadata.series[i]) + mediaMetadata.series[i].id = null continue } @@ -600,6 +604,8 @@ class ApiRouter { mediaMetadata.series[i].id = seriesItem.id } } + // Remove series without an id + mediaMetadata.series = mediaMetadata.series.filter(se => se.id) if (newSeries.length) { await Database.createBulkSeries(newSeries) SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON())) From 8cd50d56844bc525d026303896b7d77000568b74 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Fri, 29 Mar 2024 14:51:34 +0100 Subject: [PATCH 0487/2145] OpenID: Don't downgrade root --- server/Auth.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server/Auth.js b/server/Auth.js index e14348c7..59f32d7e 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -220,6 +220,16 @@ async setUserGroup(user, userinfo) { let userType = rolesInOrderOfPriority.find(role => groupsList.includes(role)) if (userType) { + if (user.type === 'root') { + // Check OpenID Group + if (userType !== 'admin') { + throw new Error(`Root user "${user.username}" cannot be downgraded to ${userType}. Denying login.`) + } else { + // If root user is logging in via OpenID, we will not change the type + return + } + } + Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`) if (user.type !== userType) { @@ -239,7 +249,7 @@ async updateUserPermissions(user, userinfo) { if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything return - if (user.type === 'admin') + if (user.type === 'admin' || user.type === 'root') return const absPermissions = userinfo[absPermissionsClaim] From 90e1283058c6916b873fcab490dc881ad0a25155 Mon Sep 17 00:00:00 2001 From: Denis Arnst <git@sapd.eu> Date: Fri, 29 Mar 2024 15:11:56 +0100 Subject: [PATCH 0488/2145] OpenID: Allow email_verified null and also check username Only disallow when email_verified explicitly false Also check username besides preferred_username, even when its not included in OIDC checks (synology uses username) --- server/Auth.js | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 59f32d7e..733acc36 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -144,22 +144,47 @@ class Auth { } // Match existing user by email - 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) + if (Database.serverSettings.authOpenIDMatchExistingBy === 'email') { + if (userinfo.email) { + // Only disallow when email_verified explicitly set to false (allow both if not set or true) + if (userinfo.email_verified === false) { + Logger.warn(`[Auth] openid: User not found and email "${userinfo.email}" is not verified`) + return null + } else { + Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`) + user = await Database.userModel.getUserByEmail(userinfo.email) - if (user?.authOpenIDSub) { - Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) - return null // User is linked to a different OpenID subject; do not proceed. + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) + return null // User is linked to a different OpenID subject; do not proceed. + } + } + } else { + Logger.warn(`[Auth] openid: User not found and no email in userinfo`) + // We deny login, because if the admin whishes to match email, it makes sense to require it + return null } } // Match existing user by username - 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) + else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username') { + let username + + if (userinfo.preferred_username) { + Logger.info(`[Auth] openid: User not found, checking existing with userinfo.preferred_username "${userinfo.preferred_username}"`) + username = userinfo.preferred_username + } else if (userinfo.username) { + Logger.info(`[Auth] openid: User not found, checking existing with userinfo.username "${userinfo.username}"`) + username = userinfo.username + } else { + Logger.warn(`[Auth] openid: User not found and neither preferred_username nor username in userinfo`) + return null + } + + + user = await Database.userModel.getUserByUsername(username) if (user?.authOpenIDSub) { - Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) + Logger.warn(`[Auth] openid: User found with username "${username}" but is already matched with sub "${user.authOpenIDSub}"`) return null // User is linked to a different OpenID subject; do not proceed. } } From aefda8bd5151eb8ee817d2f896c51da51f088cf6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 30 Mar 2024 11:40:35 -0500 Subject: [PATCH 0489/2145] Fix:Local sessions set date and dayOfWeek using the updatedAt timestamp passed in from the client #2795 --- server/managers/PlaybackSessionManager.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index 879dff7e..99ddb47a 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -144,8 +144,13 @@ class PlaybackSessionManager { session.currentTime = sessionJson.currentTime session.timeListening = sessionJson.timeListening session.updatedAt = sessionJson.updatedAt - session.date = date.format(new Date(), 'YYYY-MM-DD') - session.dayOfWeek = date.format(new Date(), 'dddd') + + let jsDate = new Date(sessionJson.updatedAt) + if (isNaN(jsDate)) { + jsDate = new Date() + } + session.date = date.format(jsDate, 'YYYY-MM-DD') + session.dayOfWeek = date.format(jsDate, 'dddd') Logger.debug(`[PlaybackSessionManager] Updated session for "${session.displayTitle}" (${session.id})`) await Database.updatePlaybackSession(session) From 7e8fd91fc5a3c7802573ae8903a1c25505f6c9c5 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 30 Mar 2024 14:04:02 -0500 Subject: [PATCH 0490/2145] Update OIDC advanced permissions check to only perform an update on changes - Update permissions example to use UUIDv4 strings for allowedLibraries - More validation on advanced permission JSON to ensure arrays are array of strings - Only set allowedTags and allowedLibraries if the corresponding access all permission is false --- server/Auth.js | 112 +++++++++++++++++++----------------- server/objects/user/User.js | 75 +++++++++++++++++------- 2 files changed, 113 insertions(+), 74 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 733acc36..b52ee727 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -100,24 +100,24 @@ class Auth { }, async (tokenset, userinfo, done) => { try { Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2)) - + if (!userinfo.sub) { throw new Error('Invalid userinfo, no sub') } - + if (!this.validateGroupClaim(userinfo)) { throw new Error(`Group claim ${Database.serverSettings.authOpenIDGroupClaim} not found or empty in userinfo`) } - + let user = await this.findOrCreateUser(userinfo) - - if (!user || !user.isActive) { + + if (!user?.isActive) { throw new Error('User not active or not found') } - + await this.setUserGroup(user, userinfo) await this.updateUserPermissions(user, userinfo) - + // We also have to save the id_token for later (used for logout) because we cannot set cookies here user.openid_id_token = tokenset.id_token @@ -229,62 +229,68 @@ class Auth { return true } -/** - * Sets the user group based on group claim in userinfo. - */ -async setUserGroup(user, userinfo) { - const groupClaimName = Database.serverSettings.authOpenIDGroupClaim - if (!groupClaimName) // No group claim configured, don't set anything - return + /** + * Sets the user group based on group claim in userinfo. + * + * @param {import('./objects/user/User')} user + * @param {Object} userinfo + */ + async setUserGroup(user, userinfo) { + const groupClaimName = Database.serverSettings.authOpenIDGroupClaim + if (!groupClaimName) // No group claim configured, don't set anything + return - if (!userinfo[groupClaimName]) - throw new Error(`Group claim ${groupClaimName} not found in userinfo`) + if (!userinfo[groupClaimName]) + throw new Error(`Group claim ${groupClaimName} not found in userinfo`) - const groupsList = userinfo[groupClaimName].map(group => group.toLowerCase()) - const rolesInOrderOfPriority = ['admin', 'user', 'guest'] + const groupsList = userinfo[groupClaimName].map(group => group.toLowerCase()) + const rolesInOrderOfPriority = ['admin', 'user', 'guest'] - let userType = rolesInOrderOfPriority.find(role => groupsList.includes(role)) - if (userType) { - if (user.type === 'root') { - // Check OpenID Group - if (userType !== 'admin') { - throw new Error(`Root user "${user.username}" cannot be downgraded to ${userType}. Denying login.`) - } else { - // If root user is logging in via OpenID, we will not change the type - return + let userType = rolesInOrderOfPriority.find(role => groupsList.includes(role)) + if (userType) { + if (user.type === 'root') { + // Check OpenID Group + if (userType !== 'admin') { + throw new Error(`Root user "${user.username}" cannot be downgraded to ${userType}. Denying login.`) + } else { + // If root user is logging in via OpenID, we will not change the type + return + } } + + if (user.type !== userType) { + Logger.info(`[Auth] openid callback: Updating user "${user.username}" type to "${userType}" from "${user.type}"`) + user.type = userType + await Database.userModel.updateFromOld(user) + } + } else { + throw new Error(`No valid group found in userinfo: ${JSON.stringify(userinfo[groupClaimName], null, 2)}`) } + } - Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`) + /** + * Updates user permissions based on the advanced permissions claim. + * + * @param {import('./objects/user/User')} user + * @param {Object} userinfo + */ + async updateUserPermissions(user, userinfo) { + const absPermissionsClaim = Database.serverSettings.authOpenIDAdvancedPermsClaim + if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything + return - if (user.type !== userType) { - user.type = userType + if (user.type === 'admin' || user.type === 'root') + return + + const absPermissions = userinfo[absPermissionsClaim] + if (!absPermissions) + throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`) + + if (user.updatePermissionsFromExternalJSON(absPermissions)) { + Logger.debug(`[Auth] openid callback: Updating advanced perms for user "${user.username}" using "${JSON.stringify(absPermissions)}"`) await Database.userModel.updateFromOld(user) } - } else { - throw new Error(`No valid group found in userinfo: ${JSON.stringify(userinfo[groupClaimName], null, 2)}`) } -} - -/** - * Updates user permissions based on the advanced permissions claim. - */ -async updateUserPermissions(user, userinfo) { - const absPermissionsClaim = Database.serverSettings.authOpenIDAdvancedPermsClaim - if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything - return - - if (user.type === 'admin' || user.type === 'root') - return - - const absPermissions = userinfo[absPermissionsClaim] - if (!absPermissions) - throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`) - - Logger.debug(`[Auth] openid callback: Updating advanced perms for user ${user.username} to ${JSON.stringify(absPermissions)}`) - user.updatePermissionsFromExternalJSON(absPermissions) - await Database.userModel.updateFromOld(user) -} /** * Unuse strategy diff --git a/server/objects/user/User.js b/server/objects/user/User.js index b473637b..938c6d07 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -280,64 +280,97 @@ class User { tagsAreDenylist: 'selectedTagsNotAccessible', // Direct mapping for array-based permissions allowedLibraries: 'librariesAccessible', - allowedTags: 'itemTagsSelected', + allowedTags: 'itemTagsSelected' } /** - * Update user from external JSON + * Update user permissions from external JSON * - * @param {object} absPermissions JSON containg user permissions + * @param {Object} absPermissions JSON containing user permissions + * @returns {boolean} true if updates were made */ updatePermissionsFromExternalJSON(absPermissions) { + let hasUpdates = false + let updatedUserPermissions = {} + // Initialize all permissions to false first Object.keys(User.permissionMapping).forEach(mappingKey => { - const userPermKey = User.permissionMapping[mappingKey]; + const userPermKey = User.permissionMapping[mappingKey] if (typeof this.permissions[userPermKey] === 'boolean') { - this.permissions[userPermKey] = false; // Default to false for boolean permissions - } else { - this[userPermKey] = []; // Default to empty array for other properties + updatedUserPermissions[userPermKey] = false // Default to false for boolean permissions } - }); + }) + // Map the boolean permissions from absPermissions Object.keys(absPermissions).forEach(absKey => { const userPermKey = User.permissionMapping[absKey] if (!userPermKey) { throw new Error(`Unexpected permission property: ${absKey}`) } - // Update the user's permissions based on absPermissions - this.permissions[userPermKey] = absPermissions[absKey] - }); + if (updatedUserPermissions[userPermKey] !== undefined) { + updatedUserPermissions[userPermKey] = !!absPermissions[absKey] + } + }) - // Handle allowedLibraries and allowedTags separately if needed - if (absPermissions.allowedLibraries) { + // Update user permissions if changes were made + if (JSON.stringify(this.permissions) !== JSON.stringify(updatedUserPermissions)) { + this.permissions = updatedUserPermissions + hasUpdates = true + } + + // Handle allowedLibraries + if (this.permissions.accessAllLibraries) { + if (this.librariesAccessible.length) { + this.librariesAccessible = [] + hasUpdates = true + } + } else if (absPermissions.allowedLibraries?.length && absPermissions.allowedLibraries.join(',') !== this.librariesAccessible.join(',')) { + if (absPermissions.allowedLibraries.some(lid => typeof lid !== 'string')) { + throw new Error('Invalid permission property "allowedLibraries", expecting array of strings') + } this.librariesAccessible = absPermissions.allowedLibraries + hasUpdates = true } - if (absPermissions.allowedTags) { + + // Handle allowedTags + if (this.permissions.accessAllTags) { + if (this.itemTagsSelected.length) { + this.itemTagsSelected = [] + hasUpdates = true + } + } else if (absPermissions.allowedTags?.length && absPermissions.allowedTags.join(',') !== this.itemTagsSelected.join(',')) { + if (absPermissions.allowedTags.some(tag => typeof tag !== 'string')) { + throw new Error('Invalid permission property "allowedTags", expecting array of strings') + } this.itemTagsSelected = absPermissions.allowedTags + hasUpdates = true } + + return hasUpdates } + /** * Get a sample to show how a JSON for updatePermissionsFromExternalJSON should look like * - * @returns JSON string + * @returns {string} JSON string */ static getSampleAbsPermissions() { // Start with a template object where all permissions are false for simplicity const samplePermissions = Object.keys(User.permissionMapping).reduce((acc, key) => { // For array-based permissions, provide a sample array if (key === 'allowedLibraries') { - acc[key] = [`ExampleLibrary`, `AnotherLibrary`]; + acc[key] = [`5406ba8a-16e1-451d-96d7-4931b0a0d966`, `918fd848-7c1d-4a02-818a-847435a879ca`] } else if (key === 'allowedTags') { - acc[key] = [`ExampleTag`, `AnotherTag`, `ThirdTag`]; + acc[key] = [`ExampleTag`, `AnotherTag`, `ThirdTag`] } else { - acc[key] = false; + acc[key] = false } - return acc; - }, {}); + return acc + }, {}) - return JSON.stringify(samplePermissions, null, 2); // Pretty print the JSON + return JSON.stringify(samplePermissions, null, 2) // Pretty print the JSON } /** From a5d7a8151917a5073595d98e84f99d543c45f079 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 30 Mar 2024 14:17:34 -0500 Subject: [PATCH 0491/2145] Clean up formatting of advanced group/permission claims on authentication page --- client/pages/config/authentication.vue | 15 ++++++++------- server/Auth.js | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index cecccee4..9f2e71ec 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -82,19 +82,20 @@ <p class="pl-4 text-sm text-gray-300">{{ $strings.LabelAutoRegisterDescription }}</p> </div> - <div class="flex items-center pt-6 pb-1 px-1 w-full">{{ $strings.LabelOpenIDClaims }}</div> - <div class="flex items-center mb-2"> - <div class="w-96"> + <p class="pt-6 mb-4 px-1">{{ $strings.LabelOpenIDClaims }}</p> + + <div class="flex mb-4"> + <div class="w-44 min-w-44"> <ui-text-input-with-label ref="openidGroupClaim" v-model="newAuthSettings.authOpenIDGroupClaim" :disabled="savingSettings" :placeholder="'groups'" :label="'Group Claim'" /> </div> - <p class="pl-4 text-sm text-gray-300 mt-5" v-html="$strings.LabelOpenIDGroupClaimDescription"></p> + <p class="pl-4 text-sm text-gray-300" v-html="$strings.LabelOpenIDGroupClaimDescription"></p> </div> - <div class="flex mb-2"> - <div class="w-96 pt-6"> + <div class="flex mb-4"> + <div class="w-44 min-w-44"> <ui-text-input-with-label ref="openidAdvancedPermsClaim" v-model="newAuthSettings.authOpenIDAdvancedPermsClaim" :disabled="savingSettings" :placeholder="'abspermissions'" :label="'Advanced Permission Claim'" /> </div> - <div class="pl-4 text-sm text-gray-300 mt-5 flex-column"> + <div class="pl-4 text-sm text-gray-300 flex-column"> <p v-html="$strings.LabelOpenIDAdvancedPermsClaimDescription"></p> <pre class="text-pre-wrap mt-2" >{{ newAuthSettings.authOpenIDSamplePermissions }} diff --git a/server/Auth.js b/server/Auth.js index b52ee727..8ba87509 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -287,7 +287,7 @@ class Auth { throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`) if (user.updatePermissionsFromExternalJSON(absPermissions)) { - Logger.debug(`[Auth] openid callback: Updating advanced perms for user "${user.username}" using "${JSON.stringify(absPermissions)}"`) + Logger.info(`[Auth] openid callback: Updating advanced perms for user "${user.username}" using "${JSON.stringify(absPermissions)}"`) await Database.userModel.updateFromOld(user) } } From fc595bd799639cd7a6a8db6186acb95eb1c0b6da Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 30 Mar 2024 14:25:38 -0500 Subject: [PATCH 0492/2145] Updates to authentication page for mobile screen sizes --- client/components/app/SettingsContent.vue | 2 +- client/pages/config/authentication.vue | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/components/app/SettingsContent.vue b/client/components/app/SettingsContent.vue index ec129ebc..40b0c02f 100644 --- a/client/components/app/SettingsContent.vue +++ b/client/components/app/SettingsContent.vue @@ -1,5 +1,5 @@ <template> - <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> + <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-2 sm:p-4 mb-8"> <div class="flex items-center mb-2"> <slot name="header-prefix"></slot> <h1 class="text-xl">{{ headerText }}</h1> diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 9f2e71ec..a85433c5 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -59,15 +59,15 @@ <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" /> + <p class="sm: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"> + <div class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2"> <div class="w-44"> <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">{{ $strings.LabelMatchExistingUsersByDescription }}</p> + <p class="sm:pl-4 text-sm text-gray-300 mt-2 sm:mt-5">{{ $strings.LabelMatchExistingUsersByDescription }}</p> </div> <div class="flex items-center py-4 px-1 w-full"> @@ -84,18 +84,18 @@ <p class="pt-6 mb-4 px-1">{{ $strings.LabelOpenIDClaims }}</p> - <div class="flex mb-4"> + <div class="flex flex-col sm:flex-row mb-4"> <div class="w-44 min-w-44"> <ui-text-input-with-label ref="openidGroupClaim" v-model="newAuthSettings.authOpenIDGroupClaim" :disabled="savingSettings" :placeholder="'groups'" :label="'Group Claim'" /> </div> - <p class="pl-4 text-sm text-gray-300" v-html="$strings.LabelOpenIDGroupClaimDescription"></p> + <p class="sm:pl-4 pt-2 sm:pt-0 text-sm text-gray-300" v-html="$strings.LabelOpenIDGroupClaimDescription"></p> </div> - <div class="flex mb-4"> + <div class="flex flex-col sm:flex-row mb-4"> <div class="w-44 min-w-44"> <ui-text-input-with-label ref="openidAdvancedPermsClaim" v-model="newAuthSettings.authOpenIDAdvancedPermsClaim" :disabled="savingSettings" :placeholder="'abspermissions'" :label="'Advanced Permission Claim'" /> </div> - <div class="pl-4 text-sm text-gray-300 flex-column"> + <div class="sm:pl-4 pt-2 sm:pt-0 text-sm text-gray-300"> <p v-html="$strings.LabelOpenIDAdvancedPermsClaimDescription"></p> <pre class="text-pre-wrap mt-2" >{{ newAuthSettings.authOpenIDSamplePermissions }} From aa1aeacc09fb68b7d98320746d308bda10450541 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 30 Mar 2024 14:26:55 -0500 Subject: [PATCH 0493/2145] Map new translation strings --- client/strings/cs.json | 3 +++ client/strings/da.json | 3 +++ client/strings/de.json | 2 +- client/strings/en-us.json | 2 +- client/strings/es.json | 3 +++ client/strings/et.json | 3 +++ client/strings/fr.json | 3 +++ client/strings/gu.json | 3 +++ client/strings/he.json | 3 +++ client/strings/hi.json | 3 +++ client/strings/hr.json | 3 +++ client/strings/hu.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/pt-br.json | 5 ++++- client/strings/ru.json | 3 +++ client/strings/sv.json | 3 +++ client/strings/vi-vn.json | 3 +++ client/strings/zh-cn.json | 3 +++ client/strings/zh-tw.json | 7 ++++++- 23 files changed, 69 insertions(+), 4 deletions(-) diff --git a/client/strings/cs.json b/client/strings/cs.json index 4ce358a2..3f4097aa 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Nezahájeno", "LabelNumberOfBooks": "Počet knih", "LabelNumberOfEpisodes": "Počet epizod", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Otevřít RSS kanál", "LabelOverwrite": "Přepsat", "LabelPassword": "Heslo", diff --git a/client/strings/da.json b/client/strings/da.json index de00a1fc..54707852 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Ikke påbegyndt", "LabelNumberOfBooks": "Antal bøger", "LabelNumberOfEpisodes": "Antal episoder", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Åbn RSS-feed", "LabelOverwrite": "Overskriv", "LabelPassword": "Kodeord", diff --git a/client/strings/de.json b/client/strings/de.json index 611432f1..7b4283f6 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -387,9 +387,9 @@ "LabelNotStarted": "Nicht begonnen", "LabelNumberOfBooks": "Anzahl der Hörbücher", "LabelNumberOfEpisodes": "Anzahl der Episoden", + "LabelOpenIDAdvancedPermsClaimDescription": "Name des OpenID-Claims, der erweiterte Berechtigungen für Benutzeraktionen innerhalb der Anwendung enthält, die auf Nicht-Admin-Rollen angewendet werden (<b>wenn konfiguriert</b>). Wenn der Claim in der Antwort fehlt, wird der Zugang zu ABS verweigert. Fehlt eine einzelne Option, wird sie als <code>false</code> behandelt. Stelle sicher, dass der Claim des Identitätsanbieters der erwarteten Struktur entspricht:", "LabelOpenIDClaims": "Lass die folgenden Optionen leer, um die erweiterte Zuweisung von Gruppen und Berechtigungen zu deaktivieren und automatisch die 'User'-Gruppe zuzuweisen.", "LabelOpenIDGroupClaimDescription": "Name des OpenID-Claims, der eine Liste der Benutzergruppen enthält. Wird häufig als <code>groups</code> bezeichnet. <b>Wenn konfiguriert</b>, wird die Anwendung automatisch Rollen basierend auf den Gruppenmitgliedschaften des Benutzers zuweisen, vorausgesetzt, dass diese Gruppen im Claim als 'admin', 'user' oder 'guest' benannt sind (Groß/Kleinschreibung ist irrelevant). Der Claim eine Liste sein, und wenn ein Benutzer mehreren Gruppen angehört, wird die Anwendung die Rolle zuordnen, die dem höchsten Zugriffslevel entspricht. Wenn keine Gruppe übereinstimmt, wird der Zugang verweigert.", - "LabelOpenIDAdvancedPermsClaimDescription": "Name des OpenID-Claims, der erweiterte Berechtigungen für Benutzeraktionen innerhalb der Anwendung enthält, die auf Nicht-Admin-Rollen angewendet werden (<b>wenn konfiguriert</b>). Wenn der Claim in der Antwort fehlt, wird der Zugang zu ABS verweigert. Fehlt eine einzelne Option, wird sie als <code>false</code> behandelt. Stelle sicher, dass der Claim des Identitätsanbieters der erwarteten Struktur entspricht:", "LabelOpenRSSFeed": "Öffne RSS-Feed", "LabelOverwrite": "Überschreiben", "LabelPassword": "Passwort", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index b6fe3505..622c3a10 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -387,9 +387,9 @@ "LabelNotStarted": "Not Started", "LabelNumberOfBooks": "Number of Books", "LabelNumberOfEpisodes": "# of Episodes", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", - "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", "LabelOpenRSSFeed": "Open RSS Feed", "LabelOverwrite": "Overwrite", "LabelPassword": "Password", diff --git a/client/strings/es.json b/client/strings/es.json index 068c5bc4..891e2976 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Sin Iniciar", "LabelNumberOfBooks": "Numero de Libros", "LabelNumberOfEpisodes": "# de Episodios", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Abrir Fuente RSS", "LabelOverwrite": "Sobrescribir", "LabelPassword": "Contraseña", diff --git a/client/strings/et.json b/client/strings/et.json index da145027..6737e7fb 100644 --- a/client/strings/et.json +++ b/client/strings/et.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Pole alustatud", "LabelNumberOfBooks": "Raamatute arv", "LabelNumberOfEpisodes": "Episoodide arv", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Ava RSS voog", "LabelOverwrite": "Kirjuta üle", "LabelPassword": "Parool", diff --git a/client/strings/fr.json b/client/strings/fr.json index b7b28472..65fdfa1f 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Pas commencé", "LabelNumberOfBooks": "Nombre de livres", "LabelNumberOfEpisodes": "Nombre d’épisodes", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Ouvrir le flux RSS", "LabelOverwrite": "Écraser", "LabelPassword": "Mot de passe", diff --git a/client/strings/gu.json b/client/strings/gu.json index 0a2dc3b6..f2554292 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Not Started", "LabelNumberOfBooks": "Number of Books", "LabelNumberOfEpisodes": "# of Episodes", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Open RSS Feed", "LabelOverwrite": "Overwrite", "LabelPassword": "Password", diff --git a/client/strings/he.json b/client/strings/he.json index 435f60ad..45ed1da8 100644 --- a/client/strings/he.json +++ b/client/strings/he.json @@ -387,6 +387,9 @@ "LabelNotStarted": "לא התחיל", "LabelNumberOfBooks": "מספר הספרים", "LabelNumberOfEpisodes": "מספר הפרקים", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "פתח ערוץ RSS", "LabelOverwrite": "לשכפל", "LabelPassword": "סיסמה", diff --git a/client/strings/hi.json b/client/strings/hi.json index 4e8aae7b..8012646c 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Not Started", "LabelNumberOfBooks": "Number of Books", "LabelNumberOfEpisodes": "# of Episodes", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Open RSS Feed", "LabelOverwrite": "Overwrite", "LabelPassword": "Password", diff --git a/client/strings/hr.json b/client/strings/hr.json index fa5bcd35..73bff153 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Not Started", "LabelNumberOfBooks": "Number of Books", "LabelNumberOfEpisodes": "# of Episodes", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Otvori RSS Feed", "LabelOverwrite": "Overwrite", "LabelPassword": "Lozinka", diff --git a/client/strings/hu.json b/client/strings/hu.json index f7e2abd8..f2829f7b 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Nem indult el", "LabelNumberOfBooks": "Könyvek száma", "LabelNumberOfEpisodes": "Epizódok száma", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "RSS hírcsatorna megnyitása", "LabelOverwrite": "Felülírás", "LabelPassword": "Jelszó", diff --git a/client/strings/it.json b/client/strings/it.json index 8924a28b..3a5f7b3b 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Non iniziato", "LabelNumberOfBooks": "Numero di libri", "LabelNumberOfEpisodes": "# degli episodi", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Apri RSS Feed", "LabelOverwrite": "Sovrascrivi", "LabelPassword": "Password", diff --git a/client/strings/lt.json b/client/strings/lt.json index d36861a4..57549dd9 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Nepasileista", "LabelNumberOfBooks": "Knygų skaičius", "LabelNumberOfEpisodes": "Epizodų skaičius", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Atidaryti RSS srautą", "LabelOverwrite": "Perrašyti", "LabelPassword": "Slaptažodis", diff --git a/client/strings/nl.json b/client/strings/nl.json index b8d3f669..c876d1f5 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Niet Gestart", "LabelNumberOfBooks": "Aantal Boeken", "LabelNumberOfEpisodes": "# afleveringen", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Open RSS-feed", "LabelOverwrite": "Overschrijf", "LabelPassword": "Wachtwoord", diff --git a/client/strings/no.json b/client/strings/no.json index 818ee0fa..08c48518 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Ikke startet", "LabelNumberOfBooks": "Antall bøker", "LabelNumberOfEpisodes": "Antall episoder", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Åpne RSS Feed", "LabelOverwrite": "Overskriv", "LabelPassword": "Passord", diff --git a/client/strings/pl.json b/client/strings/pl.json index cf4274cc..3823fb6c 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Nie rozpoęczto", "LabelNumberOfBooks": "Liczba książek", "LabelNumberOfEpisodes": "# odcinków", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Otwórz kanał RSS", "LabelOverwrite": "Overwrite", "LabelPassword": "Hasło", diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index c4d00eb7..3ca7cd78 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Não iniciado", "LabelNumberOfBooks": "Número de Livros", "LabelNumberOfEpisodes": "# de Episódios", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Abrir Feed RSS", "LabelOverwrite": "Sobrescrever", "LabelPassword": "Senha", @@ -778,4 +781,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} +} \ No newline at end of file diff --git a/client/strings/ru.json b/client/strings/ru.json index a55e4668..02883dec 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Не запущено", "LabelNumberOfBooks": "Количество книг", "LabelNumberOfEpisodes": "# Эпизодов", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Открыть RSS-канал", "LabelOverwrite": "Перезаписать", "LabelPassword": "Пароль", diff --git a/client/strings/sv.json b/client/strings/sv.json index 820fe18a..12b211a0 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Inte påbörjad", "LabelNumberOfBooks": "Antal böcker", "LabelNumberOfEpisodes": "Antal avsnitt", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Öppna RSS-flöde", "LabelOverwrite": "Skriv över", "LabelPassword": "Lösenord", diff --git a/client/strings/vi-vn.json b/client/strings/vi-vn.json index bddfd647..ffc19dff 100644 --- a/client/strings/vi-vn.json +++ b/client/strings/vi-vn.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Chưa bắt đầu", "LabelNumberOfBooks": "Số lượng Sách", "LabelNumberOfEpisodes": "# của Tập", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Mở RSS Feed", "LabelOverwrite": "Ghi đè", "LabelPassword": "Mật khẩu", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index bf816f89..cf831116 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -387,6 +387,9 @@ "LabelNotStarted": "未开始", "LabelNumberOfBooks": "图书数量", "LabelNumberOfEpisodes": "# 集", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "打开 RSS 源", "LabelOverwrite": "覆盖", "LabelPassword": "密码", diff --git a/client/strings/zh-tw.json b/client/strings/zh-tw.json index 5a9afc98..21a42095 100644 --- a/client/strings/zh-tw.json +++ b/client/strings/zh-tw.json @@ -387,6 +387,9 @@ "LabelNotStarted": "未開始", "LabelNumberOfBooks": "圖書數量", "LabelNumberOfEpisodes": "# 集", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "打開 RSS 源", "LabelOverwrite": "覆蓋", "LabelPassword": "密碼", @@ -466,6 +469,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "只有一本書的系列將從系列頁面和主頁書架中隱藏.", "LabelSettingsHomePageBookshelfView": "首頁使用書架視圖", "LabelSettingsLibraryBookshelfView": "媒體庫使用書架視圖", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "解析副標題", "LabelSettingsParseSubtitlesHelp": "從有聲書資料夾中提取副標題.<br>副標題必須用 \" - \" 分隔.<br>例: \"書名 - 這裡是副標題\" 則顯示副標題 \"這裡是副標題\"", "LabelSettingsPreferMatchedMetadata": "首選匹配的元數據", @@ -776,4 +781,4 @@ "ToastSocketFailedToConnect": "網路連接失敗", "ToastUserDeleteFailed": "刪除使用者失敗", "ToastUserDeleteSuccess": "使用者已刪除" -} +} \ No newline at end of file From afe40be957eda7d7679f69e78ff0c16f162d2af9 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Sat, 30 Mar 2024 23:47:13 +0000 Subject: [PATCH 0494/2145] Initial large file --- docs/spec.yaml | 972 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 972 insertions(+) create mode 100644 docs/spec.yaml diff --git a/docs/spec.yaml b/docs/spec.yaml new file mode 100644 index 00000000..dfe95b69 --- /dev/null +++ b/docs/spec.yaml @@ -0,0 +1,972 @@ +openapi: 3.0.0 +info: + title: Audiobookshelf API + version: 0.1.0 + description: Audiobookshelf API with autogenerated OpenAPI doc + +servers: + - url: http://localhost:3000 + description: Development server + +components: + schemas: + mediaType: + type: string + description: The type of media, will be book or podcast. + enum: [book, podcast] + mediaMinified: + description: The minified media of the library item. + oneOf: + - $ref: '#/components/schemas/bookMinified' + bookCoverPath: + description: The absolute path on the server of the cover file. Will be null if there is no cover. + type: string + nullable: true + example: /audiobooks/Terry Goodkind/Sword of Truth/Wizards First Rule/cover.jpg + bookBase: + type: object + description: Base book schema + properties: + libraryItemId: + $ref: '#/components/schemas/libraryItemId' + coverPath: + $ref: '#/components/schemas/bookCoverPath' + tags: + $ref: '#/components/schemas/tags' + audioFiles: + type: array + items: + $ref: '#/components/schemas/audioFile' + chapters: + type: array + items: + $ref: '#/components/schemas/bookChapter' + missingParts: + description: Any parts missing from the book by track index. + type: array + items: + type: integer + ebookFile: + $ref: '#/components/schemas/ebookFile' + bookMinified: + type: object + description: Minified book schema. Does not depend on `bookBase` because there's pretty much no overlap. + properties: + metadata: + $ref: '#/components/schemas/bookMetadataMinified' + coverPath: + $ref: '#/components/schemas/bookCoverPath' + tags: + $ref: '#/components/schemas/tags' + numTracks: + description: The number of tracks the book's audio files have. + type: integer + example: 1 + numAudioFiles: + description: The number of audio files the book has. + type: integer + example: 1 + numChapters: + description: The number of chapters the book has. + type: integer + example: 1 + numMissingParts: + description: The total number of missing parts the book has. + type: integer + example: 0 + numInvalidAudioFiles: + description: The number of invalid audio files the book has. + type: integer + example: 0 + duration: + $ref: '#/components/schemas/durationSec' + size: + $ref: '#/components/schemas/size' + ebookFormat: + description: The format of ebook of the book. Will be null if the book is an audiobook. + type: string + nullable: true + narrators: + description: The narrators of the audiobook. + type: array + items: + type: string + example: Sam Tsoutsouvas + bookMetadataBase: + type: object + description: The base book metadata object for minified, normal, and extended schemas to inherit from. + properties: + title: + description: The title of the book. Will be null if unknown. + type: string + nullable: true + example: Wizards First Rule + subtitle: + description: The subtitle of the book. Will be null if there is no subtitle. + type: string + nullable: true + genres: + description: The genres of the book. + type: array + items: + type: string + example: ["Fantasy", "Sci-Fi", "Nonfiction: History"] + publishedYear: + description: The year the book was published. Will be null if unknown. + type: string + nullable: true + example: '2008' + publishedDate: + description: The date the book was published. Will be null if unknown. + type: string + nullable: true + publisher: + description: The publisher of the book. Will be null if unknown. + type: string + nullable: true + example: Brilliance Audio + description: + description: A description for the book. Will be null if empty. + type: string + nullable: true + example: >- + The masterpiece that started Terry Goodkind's New York Times bestselling + epic Sword of Truth In the aftermath of the brutal murder of his father, + a mysterious woman, Kahlan Amnell, appears in Richard Cypher's forest + sanctuary seeking help...and more. His world, his very beliefs, are + shattered when ancient debts come due with thundering violence. In a + dark age it takes courage to live, and more than mere courage to + challenge those who hold dominion, Richard and Kahlan must take up that + challenge or become the next victims. Beyond awaits a bewitching land + where even the best of their hearts could betray them. Yet, Richard + fears nothing so much as what secrets his sword might reveal about his + own soul. Falling in love would destroy them - for reasons Richard can't + imagine and Kahlan dare not say. In their darkest hour, hunted + relentlessly, tormented by treachery and loss, Kahlan calls upon Richard + to reach beyond his sword - to invoke within himself something more + noble. Neither knows that the rules of battle have just changed...or + that their time has run out. Wizard's First Rule is the beginning. One + book. One Rule. Witness the birth of a legend. + isbn: + description: The ISBN of the book. Will be null if unknown. + type: string + nullable: true + asin: + description: The ASIN of the book. Will be null if unknown. + type: string + nullable: true + example: B002V0QK4C + language: + description: The language of the book. Will be null if unknown. + type: string + nullable: true + explicit: + description: Whether the book has been marked as explicit. + type: boolean + example: false + bookMetadataMinified: + type: object + description: The minified metadata for a book in the database. + allOf: + - $ref : '#/components/schemas/bookMetadataBase' + - type: object + properties: + titleIgnorePrefix: + description: The title of the book with any prefix moved to the end. + type: string + authorName: + description: The name of the book's author(s). + type: string + example: Terry Goodkind + authorNameLF: + description: The name of the book's author(s) with last names first. + type: string + example: Goodkind, Terry + narratorName: + description: The name of the audiobook's narrator(s). + type: string + example: Sam Tsoutsouvas + seriesName: + description: The name of the book's series. + type: string + example: Sword of Truth + bookChapter: + type: object + description: A book chapter. Includes the title and timestamps. + properties: + id: + description: The ID of the book chapter. + type: integer + example: 0 + start: + description: When in the book (in seconds) the chapter starts. + type: integer + example: 0 + end: + description: When in the book (in seconds) the chapter ends. + type: number + example: 6004.6675 + title: + description: The title of the chapter. + type: string + example: Wizards First Rule 01 Chapter 1 + audioFile: + type: object + description: An audio file for a book. Includes audio metadata and track numbers. + properties: + index: + description: The index of the audio file. + type: integer + example: 1 + ino: + $ref: '#/components/schemas/inode' + metadata: + $ref: '#/components/schemas/fileMetadata' + addedAt: + $ref: '#/components/schemas/addedAt' + updatedAt: + $ref: '#/components/schemas/updatedAt' + trackNumFromMeta: + description: The track number of the audio file as pulled from the file's metadata. Will be null if unknown. + type: integer + nullable: true + example: 1 + discNumFromMeta: + description: The disc number of the audio file as pulled from the file's metadata. Will be null if unknown. + type: string + nullable: true + trackNumFromFilename: + description: The track number of the audio file as determined from the file's name. Will be null if unknown. + type: integer + nullable: true + example: 1 + discNumFromFilename: + description: The disc number of the audio file as determined from the file's name. Will be null if unknown. + type: string + nullable: true + manuallyVerified: + description: Whether the audio file has been manually verified by a user. + type: boolean + invalid: + description: Whether the audio file is missing from the server. + type: boolean + exclude: + description: Whether the audio file has been marked for exclusion. + type: boolean + error: + description: Any error with the audio file. Will be null if there is none. + type: string + nullable: true + format: + description: The format of the audio file. + type: string + example: MP2/3 (MPEG audio layer 2/3) + duration: + $ref: '#/components/schemas/durationSec' + bitRate: + description: The bit rate (in bit/s) of the audio file. + type: integer + example: 64000 + language: + description: The language of the audio file. + type: string + nullable: true + codec: + description: The codec of the audio file. + type: string + example: mp3 + timeBase: + description: The time base of the audio file. + type: string + example: 1/14112000 + channels: + description: The number of channels the audio file has. + type: integer + example: 2 + channelLayout: + description: The layout of the audio file's channels. + type: string + example: stereo + chapters: + description: If the audio file is part of an audiobook, the chapters the file contains. + type: array + items: + $ref: '#/components/schemas/bookChapter' + embeddedCoverArt: + description: The type of embedded cover art in the audio file. Will be null if none exists. + type: string + nullable: true + metaTags: + $ref: '#/components/schemas/audioMetaTags' + mimeType: + description: The MIME type of the audio file. + type: string + example: audio/mpeg + ebookFile: + type: object + description: An ebook file for a library item. + nullable: true + properties: + ino: + $ref: '#/components/schemas/inode' + metadata: + $ref: '#/components/schemas/fileMetadata' + ebookFormat: + description: The ebook format of the ebook file. + type: string + example: epub + addedAt: + $ref: '#/components/schemas/addedAt' + updatedAt: + $ref: '#/components/schemas/updatedAt' + fileMetadata: + type: object + description: The metadata for a file, including the path, size, and unix timestamps of the file. + nullable: true + properties: + filename: + description: The filename of the file. + type: string + example: Wizards First Rule 01.mp3 + ext: + description: The file extension of the file. + type: string + example: .mp3 + path: + description: The absolute path on the server of the file. + type: string + example: >- + /audiobooks/Terry Goodkind/Sword of Truth/Wizards First Rule/Terry + Goodkind - SOT Bk01 - Wizards First Rule 01.mp3 + relPath: + description: The path of the file, relative to the book's or podcast's folder. + type: string + example: Wizards First Rule 01.mp3 + size: + $ref: '#/components/schemas/size' + mtimeMs: + description: The time (in ms since POSIX epoch) when the file was last modified on disk. + type: integer + example: 1632223180278 + ctimeMs: + description: The time (in ms since POSIX epoch) when the file status was changed on disk. + type: integer + example: 1645978261001 + birthtimeMs: + description: The time (in ms since POSIX epoch) when the file was created on disk. Will be 0 if unknown. + type: integer + example: 0 + audioMetaTags: + description: ID3 metadata tags pulled from the audio file on import. Only non-null tags will be returned in requests. + type: object + properties: + tagAlbum: + type: string + nullable: true + example: SOT Bk01 + tagArtist: + type: string + nullable: true + example: Terry Goodkind + tagGenre: + type: string + nullable: true + example: Audiobook Fantasy + tagTitle: + type: string + nullable: true + example: Wizards First Rule 01 + tagSeries: + type: string + nullable: true + tagSeriesPart: + type: string + nullable: true + tagTrack: + type: string + nullable: true + example: 01/20 + tagDisc: + type: string + nullable: true + tagSubtitle: + type: string + nullable: true + tagAlbumArtist: + type: string + nullable: true + example: Terry Goodkind + tagDate: + type: string + nullable: true + tagComposer: + type: string + nullable: true + example: Terry Goodkind + tagPublisher: + type: string + nullable: true + tagComment: + type: string + nullable: true + tagDescription: + type: string + nullable: true + tagEncoder: + type: string + nullable: true + tagEncodedBy: + type: string + nullable: true + tagIsbn: + type: string + nullable: true + tagLanguage: + type: string + nullable: true + tagASIN: + type: string + nullable: true + tagOverdriveMediaMarker: + type: string + nullable: true + tagOriginalYear: + type: string + nullable: true + tagReleaseCountry: + type: string + nullable: true + tagReleaseType: + type: string + nullable: true + tagReleaseStatus: + type: string + nullable: true + tagISRC: + type: string + nullable: true + tagMusicBrainzTrackId: + type: string + nullable: true + tagMusicBrainzAlbumId: + type: string + nullable: true + tagMusicBrainzAlbumArtistId: + type: string + nullable: true + tagMusicBrainzArtistId: + type: string + nullable: true + addedAt: + type: integer + description: The time (in ms since POSIX epoch) when added to the server. + example: 1633522963509 + createdAt: + type: integer + description: The time (in ms since POSIX epoch) when was created. + example: 1633522963509 + updatedAt: + type: integer + description: The time (in ms since POSIX epoch) when last updated. + example: 1633522963509 + size: + description: The total size (in bytes) of the item or file. + type: integer + example: 268824228 + durationSec: + description: The total length (in seconds) of the item or file. + type: number + example: 33854.905 + tags: + description: Tags applied to items. + type: array + items: + type: string + example: ["To Be Read", "Genre: Nonfiction"] + inode: + description: The inode of the item in the file system. + type: string + format: "[0-9]*" + example: '649644248522215260' + seriesId: + type: string + description: The ID of the series. + format: uuid + example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b + seriesName: + description: The name of the series. + type: string + example: Sword of Truth + folderId: + type: string + description: The ID of the folder. + format: uuid + example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b + folder: + type: object + description: Folder used in library + properties: + id: + $ref: '#/components/schemas/folderId' + fullPath: + description: The path on the server for the folder. (Read Only) + type: string + example: /podcasts + libraryId: + $ref: '#/components/schemas/libraryId' + addedAt: + $ref: '#/components/schemas/addedAt' + authorUpdated: + description: Whether the author was updated without errors. Will not exist if author was merged. + type: boolean + nullable: true + authorId: + type: string + description: The ID of the author. + format: uuid + example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b + authorASIN: + type: string + description: The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier. + nullable: true + example: B000APZOQA + authorName: + description: The name of the author. + type: string + example: Terry Goodkind + authorSeries: + type: object + description: Series and the included library items that an author has written. + properties: + id: + $ref: '#/components/schemas/seriesId' + name: + $ref: '#/components/schemas/seriesName' + items: + description: The items in the series. Each library item's media's metadata will have a `series` attribute, a `Series Sequence`, which is the matching series. + type: array + items: + $ref: '#/components/schemas/libraryItemMinified' + author: + description: An author object which includes a description and image path. + type: object + properties: + id: + $ref: '#/components/schemas/authorId' + asin: + $ref: '#/components/schemas/authorASIN' + name: + $ref: '#/components/schemas/authorName' + description: + description: A description of the author. Will be null if there is none. + type: string + nullable: true + example: | + Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork, + ‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20 + languages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary + tour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. Terry Goodkind's + brilliant books are character-driven stories, with a focus on the complexity of the human psyche. Goodkind + has an uncanny grasp for crafting compelling stories about people like you and me, trapped in terrifying + situations. + imagePath: + description: The absolute path for the author image located in the `metadata/` directory. Will be null if there is no image. + type: string + nullable: true + example: /metadata/authors/aut_bxxbyjiptmgb56yzoz.jpg + addedAt: + $ref: '#/components/schemas/addedAt' + updatedAt: + $ref: '#/components/schemas/updatedAt' + authorWithItems: + type: object + description: The author schema with an array of items they are associated with. + allOf: + - $ref: '#/components/schemas/author' + - type: object + properties: + libraryItems: + description: The items associated with the author + type: array + items: + $ref: '#/components/schemas/libraryItemMinified' + authorWithSeries: + type: object + description: The author schema with an array of items and series they are associated with. + allOf: + - $ref: '#/components/schemas/authorWithItems' + - type: object + properties: + series: + description: The series associated with the author + type: array + items: + $ref: '#/components/schemas/authorSeries' + authorMinified: + type: object + description: Minified author object which only contains the author name and ID. + properties: + id: + $ref: '#/components/schemas/authorId' + name: + $ref: '#/components/schemas/authorName' + authorExpanded: + type: object + description: The author schema with the total number of books in the library. + allOf: + - $ref: '#/components/schemas/author' + - type: object + properties: + numBooks: + description: The number of books associated with the author in the library. + type: integer + example: 1 + oldLibraryId: + type: string + description: The ID of the libraries created on server version 2.2.23 and before. + format: "lib_[a-z0-9]{18}" + example: lib_o78uaoeuh78h6aoeif + libraryId: + type: string + description: The ID of the library. + format: uuid + example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b + oldLibraryItemId: + description: The ID of library items on server version 2.2.23 and before. + type: string + nullable: true + format: "li_[a-z0-9]{18}" + example: li_o78uaoeuh78h6aoeif + libraryItemId: + type: string + description: The ID of library items after 2.3.0. + format: uuid + example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b + libraryItemBase: + type: object + description: Base library item schema + properties: + id: + $ref: '#/components/schemas/libraryItemId' + oldLibraryItemId: + $ref: '#/components/schemas/oldLibraryItemId' + ino: + $ref: '#/components/schemas/inode' + libraryId: + $ref: '#/components/schemas/libraryId' + folderId: + $ref: '#/components/schemas/folderId' + path: + description: The path of the library item on the server. + type: string + relPath: + description: The path, relative to the library folder, of the library item. + type: string + isFile: + description: Whether the library item is a single file in the root of the library folder. + type: boolean + mtimeMs: + description: The time (in ms since POSIX epoch) when the library item was last modified on disk. + type: integer + ctimeMs: + description: The time (in ms since POSIX epoch) when the library item status was changed on disk. + type: integer + birthtimeMs: + description: The time (in ms since POSIX epoch) when the library item was created on disk. Will be 0 if unknown. + type: integer + addedAt: + $ref: '#/components/schemas/addedAt' + updatedAt: + $ref: '#/components/schemas/updatedAt' + isMissing: + description: Whether the library item was scanned and no longer exists. + type: boolean + isInvalid: + description: Whether the library item was scanned and no longer has media files. + type: boolean + mediaType: + $ref: '#/components/schemas/mediaType' + libraryItemMinified: + type: object + description: A single item on the server, like a book or podcast. Minified media format. + allOf: + - $ref : '#/components/schemas/libraryItemBase' + - type: object + properties: + media: + $ref: '#/components/schemas/mediaMinified' + parameters: + authorID: + name: id + in: path + description: Author ID + required: true + schema: + $ref: '#/components/schemas/authorId' + authorInclude: + name: include + in: query + description: A comma separated list of what to include with the author. The options are `items` and `series`. `series` will only have an effect if `items` is included. + required: false + schema: + type: string + example: "items" + examples: + empty: + summary: Do not return library items + value: "" + itemOnly: + summary: Only return library items + value: "items" + itemsAndSeries: + summary: Return library items and series + value: "items,series" + authorLibraryId: + name: library + in: query + description: The ID of the library to to include filter included items from. + required: false + schema: + $ref: '#/components/schemas/libraryId' + asin: + name: asin + in: query + description: The Audible Identifier (ASIN). + required: false + schema: + $ref: '#/components/schemas/authorASIN' + authorSearchName: + name: q + in: query + description: The name of the author to use for searching. + required: false + schema: + type: string + example: Terry Goodkind + authorName: + name: name + in: query + description: The new name of the author. + required: false + schema: + $ref: '#/components/schemas/authorName' + authorDescription: + name: description + in: query + description: The new description of the author. + required: false + schema: + type: string + nullable: true + example: Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork, ‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20 languages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary tour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. + authorImagePath: + name: imagePath + in: query + description: The new absolute path for the author image. + required: false + schema: + type: string + nullable: true + example: /metadata/authors/aut_z3leimgybl7uf3y4ab.jpg + imageURL: + name: url + in: query + description: The URL of the image to add to the server + required: true + schema: + type: string + format: uri + example: https://images-na.ssl-images-amazon.com/images/I/51NoQTm33OL.__01_SX120_CR0,0,120,120__.jpg + imageWidth: + name: width + in: query + description: The requested width of image in pixels. + schema: + type: integer + default: 400 + example: 400 + example: 400 + imageHeight: + name: height + in: query + description: The requested height of image in pixels. If `null`, the height is scaled to maintain aspect ratio based on the requested width. + schema: + type: integer + nullable: true + default: null + example: 600 + examples: + scaleHeight: + summary: Scale height with width + value: null + fixedHeight: + summary: Force height of image + value: 600 + imageFormat: + name: format + in: query + description: The requested output format. + schema: + type: string + default: jpeg + example: webp + imageRaw: + name: raw + in: query + description: Return the raw image without scaling if true. + schema: + type: boolean + default: false + responses: + ok200: + description: OK + author404: + description: Author not found. + content: + text/html: + schema: + type: string + example: Not found + paths: + /api/authors/{id}: + get: + operationId: getAuthorByID + summary: Get a single author by ID on server + tags: + - Authors + parameters: + - $ref: '#/components/parameters/authorID' + - $ref: '#/components/parameters/authorInclude' + - $ref: '#/components/parameters/authorLibraryId' + responses: + 200: + description: getAuthorByID OK + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/author' + - $ref: '#/components/schemas/authorWithItems' + - $ref: '#/components/schemas/authorWithSeries' + 404: + $ref: '#/components/responses/author404' + /api/authors/{id}: + patch: + operationId: updateAuthorByID + summary: Update a single author by ID on server. This endpoint will merge two authors if the new author name matches another author in the database. + tags: + - Authors + parameters: + - $ref: '#/components/parameters/authorID' + - $ref: '#/components/parameters/asin' + - $ref: '#/components/parameters/authorName' + - $ref: '#/components/parameters/authorDescription' + - $ref: '#/components/parameters/authorImagePath' + responses: + 200: + description: updateAuthorByID OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/author' + - $ref: '#/components/schemas/authorUpdated' + - type: object + properties: + merged: + description: Will only exist and be `true` if the author was merged with another author + type: boolean + nullable: true + 404: + $ref: '#/components/responses/author404' + /api/authors/{id}: + delete: + operationId: deleteAuthorByID + summary: Delete a single author by ID on server and remove author from all books. + tags: + - Authors + parameters: + - $ref: '#/components/parameters/authorID' + responses: + 200: + $ref: '#/components/responses/ok200' + 404: + $ref: '#/components/responses/author404' + /api/authors/{id}/image: + post: + operationId: setAuthorImageByID + summary: Set an author image using a provided URL. + tags: + - Authors + parameters: + - $ref: '#/components/parameters/authorID' + - $ref: '#/components/parameters/imageURL' + responses: + 200: + description: setAuthorImageByID OK + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/author' + 404: + $ref: '#/components/responses/author404' + /api/authors/{id}/image: + delete: + operationId: deleteAuthorImageByID + summary: Delete an author image from the server and remove the image from the database. + tags: + - Authors + parameters: + - $ref: '#/components/parameters/authorID' + responses: + 200: + $ref: '#/components/responses/ok200' + 404: + $ref: '#/components/responses/author404' + /api/authors/{id}/match: + post: + operationId: matchAuthorByID + summary: Match the author against Audible using quick match. Quick match updates the author's description and image (if no image already existed) with information from audible. Either `asin` or `q` must be provided, with `asin` taking priority if both are provided. + tags: + - Authors + parameters: + - $ref: '#/components/parameters/authorID' + - $ref: '#/components/parameters/asin' + - $ref: '#/components/parameters/authorSearchName' + responses: + 200: + description: matchAuthorByID OK + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/author' + - $ref: '#/components/schemas/authorUpdated' + 404: + $ref: '#/components/responses/author404' + /api/authors/{id}/image: + patch: + operationId: getAuthorImageByID + summary: Return the author image by author ID. + tags: + - Authors + parameters: + - $ref: '#/components/parameters/authorID' + - $ref: '#/components/parameters/imageWidth' + - $ref: '#/components/parameters/imageHeight' + - $ref: '#/components/parameters/imageFormat' + - $ref: '#/components/parameters/imageRaw' + responses: + 200: + description: getAuthorImageByID OK + content: + image/*: + schema: + type: string + format: binary + 404: + $ref: '#/components/responses/author404' +tags: + - name: Authors + description: Author endpoints From c7cc9945322eb0eb35ce5baf111a11da147a0d21 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 31 Mar 2024 14:57:55 -0500 Subject: [PATCH 0495/2145] Fix:Handle enabling/disabling library watchers #2775 --- server/Watcher.js | 25 +++++++++++++++++++++---- server/controllers/LibraryController.js | 7 +++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/server/Watcher.js b/server/Watcher.js index 99318a7e..de372865 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -103,15 +103,28 @@ class FolderWatcher extends EventEmitter { this.buildLibraryWatcher(library) } + /** + * + * @param {import('./objects/Library')} library + */ updateLibrary(library) { - if (this.disabled || library.settings.disableWatcher) return - var libwatcher = this.libraryWatchers.find(lib => lib.id === library.id) + if (this.disabled) return + + const libwatcher = this.libraryWatchers.find(lib => lib.id === library.id) if (libwatcher) { + // Library watcher was disabled + if (library.settings.disableWatcher) { + Logger.info(`[Watcher] updateLibrary: Library "${library.name}" watcher disabled`) + libwatcher.watcher.close() + this.libraryWatchers = this.libraryWatchers.filter(lw => lw.id !== libwatcher.id) + return + } + libwatcher.name = library.name // If any folder paths were added or removed then re-init watcher - var pathsToAdd = library.folderPaths.filter(path => !libwatcher.paths.includes(path)) - var pathsRemoved = libwatcher.paths.filter(path => !library.folderPaths.includes(path)) + const pathsToAdd = library.folderPaths.filter(path => !libwatcher.paths.includes(path)) + const pathsRemoved = libwatcher.paths.filter(path => !library.folderPaths.includes(path)) if (pathsToAdd.length || pathsRemoved.length) { Logger.info(`[Watcher] Re-Initializing watcher for "${library.name}".`) @@ -119,6 +132,10 @@ class FolderWatcher extends EventEmitter { this.libraryWatchers = this.libraryWatchers.filter(lw => lw.id !== libwatcher.id) this.buildLibraryWatcher(library) } + } else if (!library.settings.disableWatcher) { + // Library watcher was enabled + Logger.info(`[Watcher] updateLibrary: Library "${library.name}" watcher enabled - initializing`) + this.buildLibraryWatcher(library) } } diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index a0233dd2..30c9c0d9 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -128,7 +128,14 @@ class LibraryController { res.json(libraryDownloadQueueDetails) } + /** + * PATCH: /api/libraries/:id + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ async update(req, res) { + /** @type {import('../objects/Library')} */ const library = req.library // Validate that the custom provider exists if given any From 3264359771e8252cc1746f8df357fa31c59ac5e8 Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Sun, 31 Mar 2024 19:44:53 -0300 Subject: [PATCH 0496/2145] [PT-BR] OpenID permission strings --- client/strings/pt-br.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index cc8d6ee0..c97a565d 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -385,9 +385,9 @@ "LabelNotStarted": "Não iniciado", "LabelNumberOfBooks": "Número de Livros", "LabelNumberOfEpisodes": "# de Episódios", - "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", - "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", - "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", + "LabelOpenIDAdvancedPermsClaimDescription": "Nome do claim OpenID contendo as permissões avançadas para ações do usuário na aplicação para serem aplicadas aos perfis não-administradores (<b>se configurados</b>). Se o claim não estiver presente na resposta, acesso ao ABS será negado. Se apenas uma opção estiver ausente, ela será tratada como <code>false</code>. Garanta que o claim do provedor de identidade segue a estrutura esperada:", + "LabelOpenIDClaims": "Deixe as opções a seguir em branco para desativar a atribuição de grupos e permissões avançadas; nesse caso, o grupo 'Usuário' será atribuído automaticamente.", + "LabelOpenIDGroupClaimDescription": "Nome do claim OpenID contendo a lista de grupos do usuário, normalmente chamada de <code>groups</code>. <b>Se configurada</b>, a aplicação atribuirá automaticamente os perfis com base na participação do usuário nos grupos, contanto que os nomes desses grupos no claim, sem distinção entre maiúsculas e minúsculas, sejam 'admin', 'user' ou 'guest'. O claim deve conter uma lista e, se o usuário pertencer a múltiplos grupos, a aplicação atribuirá o perfil correspondendo ao maior nível de acesso. Se não houver correspondência a qualquer grupo, o acesso será negado.", "LabelOpenRSSFeed": "Abrir Feed RSS", "LabelOverwrite": "Sobrescrever", "LabelPassword": "Senha", @@ -779,4 +779,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} \ No newline at end of file +} From c7ac12a67a3b3bf9fbd4b64b333d1f8563bb4434 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Sun, 31 Mar 2024 22:47:14 +0000 Subject: [PATCH 0497/2145] Split schema to sub files --- docs/controllers/AuthorController.yaml | 139 +++ docs/objects/Folder.yaml | 21 + docs/objects/Library.yaml | 12 + docs/objects/LibraryItem.yaml | 66 ++ docs/objects/entities/Author.yaml | 104 ++ docs/objects/entities/Series.yaml | 11 + docs/objects/files/AudioFile.yaml | 94 ++ docs/objects/mediaTypes/Book.yaml | 70 ++ docs/objects/mediaTypes/media.yaml | 10 + docs/objects/metadata/AudioMetaTags.yaml | 103 ++ docs/objects/metadata/BookMetadata.yaml | 126 +++ docs/objects/metadata/FileMetadata.yaml | 39 + docs/schemas.yaml | 33 + docs/spec.yaml | 1093 +++------------------- 14 files changed, 964 insertions(+), 957 deletions(-) create mode 100644 docs/controllers/AuthorController.yaml create mode 100644 docs/objects/Folder.yaml create mode 100644 docs/objects/Library.yaml create mode 100644 docs/objects/LibraryItem.yaml create mode 100644 docs/objects/entities/Author.yaml create mode 100644 docs/objects/entities/Series.yaml create mode 100644 docs/objects/files/AudioFile.yaml create mode 100644 docs/objects/mediaTypes/Book.yaml create mode 100644 docs/objects/mediaTypes/media.yaml create mode 100644 docs/objects/metadata/AudioMetaTags.yaml create mode 100644 docs/objects/metadata/BookMetadata.yaml create mode 100644 docs/objects/metadata/FileMetadata.yaml create mode 100644 docs/schemas.yaml diff --git a/docs/controllers/AuthorController.yaml b/docs/controllers/AuthorController.yaml new file mode 100644 index 00000000..4e576af7 --- /dev/null +++ b/docs/controllers/AuthorController.yaml @@ -0,0 +1,139 @@ +components: + schemas: + authorUpdated: + description: Whether the author was updated without errors. Will not exist if author was merged. + type: boolean + nullable: true + parameters: + authorID: + name: id + in: path + description: Author ID + required: true + schema: + $ref: '../objects/entities/Author.yaml#/components/schemas/authorId' + authorInclude: + name: include + in: query + description: A comma separated list of what to include with the author. The options are `items` and `series`. `series` will only have an effect if `items` is included. + required: false + schema: + type: string + example: "items" + examples: + empty: + summary: Do not return library items + value: "" + itemOnly: + summary: Only return library items + value: "items" + itemsAndSeries: + summary: Return library items and series + value: "items,series" + authorLibraryId: + name: library + in: query + description: The ID of the library to to include filter included items from. + required: false + schema: + $ref: '../objects/Library.yaml#/components/schemas/libraryId' + asin: + name: asin + in: query + description: The Audible Identifier (ASIN). + required: false + schema: + $ref: '../objects/entities/Author.yaml#/components/schemas/authorASIN' + authorSearchName: + name: q + in: query + description: The name of the author to use for searching. + required: false + schema: + type: string + example: Terry Goodkind + authorName: + name: name + in: query + description: The new name of the author. + required: false + schema: + $ref: '../objects/entities/Author.yaml#/components/schemas/authorName' + authorDescription: + name: description + in: query + description: The new description of the author. + required: false + schema: + type: string + nullable: true + example: Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork, ‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20 languages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary tour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. + authorImagePath: + name: imagePath + in: query + description: The new absolute path for the author image. + required: false + schema: + type: string + nullable: true + example: /metadata/authors/aut_z3leimgybl7uf3y4ab.jpg + imageURL: + name: url + in: query + description: The URL of the image to add to the server + required: true + schema: + type: string + format: uri + example: https://images-na.ssl-images-amazon.com/images/I/51NoQTm33OL.__01_SX120_CR0,0,120,120__.jpg + imageWidth: + name: width + in: query + description: The requested width of image in pixels. + schema: + type: integer + default: 400 + example: 400 + example: 400 + imageHeight: + name: height + in: query + description: The requested height of image in pixels. If `null`, the height is scaled to maintain aspect ratio based on the requested width. + schema: + type: integer + nullable: true + default: null + example: 600 + examples: + scaleHeight: + summary: Scale height with width + value: null + fixedHeight: + summary: Force height of image + value: 600 + imageFormat: + name: format + in: query + description: The requested output format. + schema: + type: string + default: jpeg + example: webp + imageRaw: + name: raw + in: query + description: Return the raw image without scaling if true. + schema: + type: boolean + default: false + responses: + author404: + description: Author not found. + content: + text/html: + schema: + type: string + example: Not found +tags: + - name: Authors + description: Author endpoints diff --git a/docs/objects/Folder.yaml b/docs/objects/Folder.yaml new file mode 100644 index 00000000..5c477b35 --- /dev/null +++ b/docs/objects/Folder.yaml @@ -0,0 +1,21 @@ +components: + schemas: + folderId: + type: string + description: The ID of the folder. + format: uuid + example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b + folder: + type: object + description: Folder used in library + properties: + id: + $ref: '#/components/schemas/folderId' + fullPath: + description: The path on the server for the folder. (Read Only) + type: string + example: /podcasts + libraryId: + $ref: './Library.yaml#/components/schemas/libraryId' + addedAt: + $ref: '../schemas.yaml#/components/schemas/addedAt' diff --git a/docs/objects/Library.yaml b/docs/objects/Library.yaml new file mode 100644 index 00000000..7d2a7833 --- /dev/null +++ b/docs/objects/Library.yaml @@ -0,0 +1,12 @@ +components: + schemas: + oldLibraryId: + type: string + description: The ID of the libraries created on server version 2.2.23 and before. + format: "lib_[a-z0-9]{18}" + example: lib_o78uaoeuh78h6aoeif + libraryId: + type: string + description: The ID of the library. + format: uuid + example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b diff --git a/docs/objects/LibraryItem.yaml b/docs/objects/LibraryItem.yaml new file mode 100644 index 00000000..5d2f2b3d --- /dev/null +++ b/docs/objects/LibraryItem.yaml @@ -0,0 +1,66 @@ +components: + schemas: + oldLibraryItemId: + description: The ID of library items on server version 2.2.23 and before. + type: string + nullable: true + format: "li_[a-z0-9]{18}" + example: li_o78uaoeuh78h6aoeif + libraryItemId: + type: string + description: The ID of library items after 2.3.0. + format: uuid + example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b + libraryItemBase: + type: object + description: Base library item schema + properties: + id: + $ref: '#/components/schemas/libraryItemId' + oldLibraryItemId: + $ref: '#/components/schemas/oldLibraryItemId' + ino: + $ref: '../schemas.yaml#/components/schemas/inode' + libraryId: + $ref: './Library.yaml#/components/schemas/libraryId' + folderId: + $ref: './Folder.yaml#/components/schemas/folderId' + path: + description: The path of the library item on the server. + type: string + relPath: + description: The path, relative to the library folder, of the library item. + type: string + isFile: + description: Whether the library item is a single file in the root of the library folder. + type: boolean + mtimeMs: + description: The time (in ms since POSIX epoch) when the library item was last modified on disk. + type: integer + ctimeMs: + description: The time (in ms since POSIX epoch) when the library item status was changed on disk. + type: integer + birthtimeMs: + description: The time (in ms since POSIX epoch) when the library item was created on disk. Will be 0 if unknown. + type: integer + addedAt: + $ref: '../schemas.yaml#/components/schemas/addedAt' + updatedAt: + $ref: '../schemas.yaml#/components/schemas/updatedAt' + isMissing: + description: Whether the library item was scanned and no longer exists. + type: boolean + isInvalid: + description: Whether the library item was scanned and no longer has media files. + type: boolean + mediaType: + $ref: './mediaTypes/media.yaml#/components/schemas/mediaType' + libraryItemMinified: + type: object + description: A single item on the server, like a book or podcast. Minified media format. + allOf: + - $ref : '#/components/schemas/libraryItemBase' + - type: object + properties: + media: + $ref: './mediaTypes/media.yaml#/components/schemas/mediaMinified' diff --git a/docs/objects/entities/Author.yaml b/docs/objects/entities/Author.yaml new file mode 100644 index 00000000..d2b173fc --- /dev/null +++ b/docs/objects/entities/Author.yaml @@ -0,0 +1,104 @@ +components: + schemas: + authorId: + type: string + description: The ID of the author. + format: uuid + example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b + authorASIN: + type: string + description: The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier. + nullable: true + example: B000APZOQA + authorName: + description: The name of the author. + type: string + example: Terry Goodkind + authorSeries: + type: object + description: Series and the included library items that an author has written. + properties: + id: + $ref: './Series.yaml#/components/schemas/seriesId' + name: + $ref: './Series.yaml#/components/schemas/seriesName' + items: + description: The items in the series. Each library item's media's metadata will have a `series` attribute, a `Series Sequence`, which is the matching series. + type: array + items: + ref: '../LibraryItem.yaml#/components/schemas/libraryItemMinified' + author: + description: An author object which includes a description and image path. + type: object + properties: + id: + $ref: '#/components/schemas/authorId' + asin: + $ref: '#/components/schemas/authorASIN' + name: + $ref: '#/components/schemas/authorName' + description: + description: A description of the author. Will be null if there is none. + type: string + nullable: true + example: | + Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork, + ‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20 + languages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary + tour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. Terry Goodkind's + brilliant books are character-driven stories, with a focus on the complexity of the human psyche. Goodkind + has an uncanny grasp for crafting compelling stories about people like you and me, trapped in terrifying + situations. + imagePath: + description: The absolute path for the author image located in the `metadata/` directory. Will be null if there is no image. + type: string + nullable: true + example: /metadata/authors/aut_bxxbyjiptmgb56yzoz.jpg + addedAt: + $ref: '../../schemas.yaml#/components/schemas/addedAt' + updatedAt: + $ref: '../../schemas.yaml#/components/schemas/updatedAt' + authorWithItems: + type: object + description: The author schema with an array of items they are associated with. + allOf: + - $ref: '#/components/schemas/author' + - type: object + properties: + libraryItems: + description: The items associated with the author + type: string + type: array + items: + $ref: '../LibraryItem.yaml#/components/schemas/libraryItemMinified' + authorWithSeries: + type: object + description: The author schema with an array of items and series they are associated with. + allOf: + - $ref: '#/components/schemas/authorWithItems' + - type: object + properties: + series: + description: The series associated with the author + type: array + items: + $ref: '#/components/schemas/authorSeries' + authorMinified: + type: object + description: Minified author object which only contains the author name and ID. + properties: + id: + $ref: '#/components/schemas/authorId' + name: + $ref: '#/components/schemas/authorName' + authorExpanded: + type: object + description: The author schema with the total number of books in the library. + allOf: + - $ref: '#/components/schemas/author' + - type: object + properties: + numBooks: + description: The number of books associated with the author in the library. + type: integer + example: 1 \ No newline at end of file diff --git a/docs/objects/entities/Series.yaml b/docs/objects/entities/Series.yaml new file mode 100644 index 00000000..af818477 --- /dev/null +++ b/docs/objects/entities/Series.yaml @@ -0,0 +1,11 @@ +components: + schemas: + seriesId: + type: string + description: The ID of the series. + format: uuid + example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b + seriesName: + description: The name of the series. + type: string + example: Sword of Truth \ No newline at end of file diff --git a/docs/objects/files/AudioFile.yaml b/docs/objects/files/AudioFile.yaml new file mode 100644 index 00000000..06a9c80c --- /dev/null +++ b/docs/objects/files/AudioFile.yaml @@ -0,0 +1,94 @@ +components: + schemas: + audioFile: + type: object + description: An audio file for a book. Includes audio metadata and track numbers. + properties: + index: + description: The index of the audio file. + type: integer + example: 1 + ino: + $ref: '../../schemas.yaml#/components/schemas/inode' + metadata: + $ref: '../metadata/FileMetadata.yaml#/components/schemas/fileMetadata' + addedAt: + $ref: '../../schemas.yaml#/components/schemas/addedAt' + updatedAt: + $ref: '../../schemas.yaml#/components/schemas/updatedAt' + trackNumFromMeta: + description: The track number of the audio file as pulled from the file's metadata. Will be null if unknown. + type: integer + nullable: true + example: 1 + discNumFromMeta: + description: The disc number of the audio file as pulled from the file's metadata. Will be null if unknown. + type: string + nullable: true + trackNumFromFilename: + description: The track number of the audio file as determined from the file's name. Will be null if unknown. + type: integer + nullable: true + example: 1 + discNumFromFilename: + description: The disc number of the audio file as determined from the file's name. Will be null if unknown. + type: string + nullable: true + manuallyVerified: + description: Whether the audio file has been manually verified by a user. + type: boolean + invalid: + description: Whether the audio file is missing from the server. + type: boolean + exclude: + description: Whether the audio file has been marked for exclusion. + type: boolean + error: + description: Any error with the audio file. Will be null if there is none. + type: string + nullable: true + format: + description: The format of the audio file. + type: string + example: MP2/3 (MPEG audio layer 2/3) + duration: + $ref: '#/components/schemas/durationSec' + bitRate: + description: The bit rate (in bit/s) of the audio file. + type: integer + example: 64000 + language: + description: The language of the audio file. + type: string + nullable: true + codec: + description: The codec of the audio file. + type: string + example: mp3 + timeBase: + description: The time base of the audio file. + type: string + example: 1/14112000 + channels: + description: The number of channels the audio file has. + type: integer + example: 2 + channelLayout: + description: The layout of the audio file's channels. + type: string + example: stereo + chapters: + description: If the audio file is part of an audiobook, the chapters the file contains. + type: array + items: + $ref: '../metadata/BookMetadata.yaml#/components/schemas/bookChapter' + embeddedCoverArt: + description: The type of embedded cover art in the audio file. Will be null if none exists. + type: string + nullable: true + metaTags: + $ref: '../metadata/AudioMetaTags.yaml#/components/schemas/audioMetaTags' + mimeType: + description: The MIME type of the audio file. + type: string + example: audio/mpeg diff --git a/docs/objects/mediaTypes/Book.yaml b/docs/objects/mediaTypes/Book.yaml new file mode 100644 index 00000000..44f3552e --- /dev/null +++ b/docs/objects/mediaTypes/Book.yaml @@ -0,0 +1,70 @@ +components: + schemas: + bookCoverPath: + description: The absolute path on the server of the cover file. Will be null if there is no cover. + type: string + nullable: true + example: /audiobooks/Terry Goodkind/Sword of Truth/Wizards First Rule/cover.jpg + bookBase: + type: object + description: Base book schema + properties: + libraryItemId: + $ref: '../LibraryItem.yaml#/components/schemas/libraryItemId' + coverPath: + $ref: '#/components/schemas/bookCoverPath' + tags: + $ref: '../../schemas.yaml#/components/schemas/tags' + audioFiles: + type: array + items: + $ref: '#/components/schemas/audioFile' + chapters: + type: array + items: + $ref: '#/components/schemas/bookChapter' + missingParts: + description: Any parts missing from the book by track index. + type: array + items: + type: integer + ebookFile: + $ref: '#/components/schemas/ebookFile' + bookMinified: + type: object + description: Minified book schema. Does not depend on `bookBase` because there's pretty much no overlap. + properties: + metadata: + $ref: '../metadata/BookMetadata.yaml#/components/schemas/bookMetadataMinified' + coverPath: + $ref: '#/components/schemas/bookCoverPath' + tags: + $ref: '../../schemas.yaml#/components/schemas/tags' + numTracks: + description: The number of tracks the book's audio files have. + type: integer + example: 1 + numAudioFiles: + description: The number of audio files the book has. + type: integer + example: 1 + numChapters: + description: The number of chapters the book has. + type: integer + example: 1 + numMissingParts: + description: The total number of missing parts the book has. + type: integer + example: 0 + numInvalidAudioFiles: + description: The number of invalid audio files the book has. + type: integer + example: 0 + duration: + $ref: '../../schemas.yaml#/components/schemas/durationSec' + size: + $ref: '../../schemas.yaml#/components/schemas/size' + ebookFormat: + description: The format of ebook of the book. Will be null if the book is an audiobook. + type: string + nullable: true diff --git a/docs/objects/mediaTypes/media.yaml b/docs/objects/mediaTypes/media.yaml new file mode 100644 index 00000000..a7d4b08e --- /dev/null +++ b/docs/objects/mediaTypes/media.yaml @@ -0,0 +1,10 @@ +components: + schemas: + mediaType: + type: string + description: The type of media, will be book or podcast. + enum: [book, podcast] + mediaMinified: + description: The minified media of the library item. + oneOf: + - $ref: './Book.yaml#/components/schemas/bookMinified' diff --git a/docs/objects/metadata/AudioMetaTags.yaml b/docs/objects/metadata/AudioMetaTags.yaml new file mode 100644 index 00000000..ef4058a8 --- /dev/null +++ b/docs/objects/metadata/AudioMetaTags.yaml @@ -0,0 +1,103 @@ +components: + schemas: + audioMetaTags: + description: ID3 metadata tags pulled from the audio file on import. Only non-null tags will be returned in requests. + type: object + properties: + tagAlbum: + type: string + nullable: true + example: SOT Bk01 + tagArtist: + type: string + nullable: true + example: Terry Goodkind + tagGenre: + type: string + nullable: true + example: Audiobook Fantasy + tagTitle: + type: string + nullable: true + example: Wizards First Rule 01 + tagSeries: + type: string + nullable: true + tagSeriesPart: + type: string + nullable: true + tagTrack: + type: string + nullable: true + example: 01/20 + tagDisc: + type: string + nullable: true + tagSubtitle: + type: string + nullable: true + tagAlbumArtist: + type: string + nullable: true + example: Terry Goodkind + tagDate: + type: string + nullable: true + tagComposer: + type: string + nullable: true + example: Terry Goodkind + tagPublisher: + type: string + nullable: true + tagComment: + type: string + nullable: true + tagDescription: + type: string + nullable: true + tagEncoder: + type: string + nullable: true + tagEncodedBy: + type: string + nullable: true + tagIsbn: + type: string + nullable: true + tagLanguage: + type: string + nullable: true + tagASIN: + type: string + nullable: true + tagOverdriveMediaMarker: + type: string + nullable: true + tagOriginalYear: + type: string + nullable: true + tagReleaseCountry: + type: string + nullable: true + tagReleaseType: + type: string + nullable: true + tagReleaseStatus: + type: string + nullable: true + tagISRC: + type: string + nullable: true + tagMusicBrainzTrackId: + type: string + nullable: true + tagMusicBrainzAlbumId: + type: string + nullable: true + tagMusicBrainzAlbumArtistId: + type: string + nullable: true + tagMusicBrainzArtistId: + type: string + nullable: true diff --git a/docs/objects/metadata/BookMetadata.yaml b/docs/objects/metadata/BookMetadata.yaml new file mode 100644 index 00000000..faa054ac --- /dev/null +++ b/docs/objects/metadata/BookMetadata.yaml @@ -0,0 +1,126 @@ +components: + schemas: + narrators: + description: The narrators of the audiobook. + type: array + items: + type: string + example: Sam Tsoutsouvas + bookMetadataBase: + type: object + description: The base book metadata object for minified, normal, and extended schemas to inherit from. + properties: + title: + description: The title of the book. Will be null if unknown. + type: string + nullable: true + example: Wizards First Rule + subtitle: + description: The subtitle of the book. Will be null if there is no subtitle. + type: string + nullable: true + genres: + description: The genres of the book. + type: array + items: + type: string + example: ["Fantasy", "Sci-Fi", "Nonfiction: History"] + publishedYear: + description: The year the book was published. Will be null if unknown. + type: string + nullable: true + example: '2008' + publishedDate: + description: The date the book was published. Will be null if unknown. + type: string + nullable: true + publisher: + description: The publisher of the book. Will be null if unknown. + type: string + nullable: true + example: Brilliance Audio + description: + description: A description for the book. Will be null if empty. + type: string + nullable: true + example: >- + The masterpiece that started Terry Goodkind's New York Times bestselling + epic Sword of Truth In the aftermath of the brutal murder of his father, + a mysterious woman, Kahlan Amnell, appears in Richard Cypher's forest + sanctuary seeking help...and more. His world, his very beliefs, are + shattered when ancient debts come due with thundering violence. In a + dark age it takes courage to live, and more than mere courage to + challenge those who hold dominion, Richard and Kahlan must take up that + challenge or become the next victims. Beyond awaits a bewitching land + where even the best of their hearts could betray them. Yet, Richard + fears nothing so much as what secrets his sword might reveal about his + own soul. Falling in love would destroy them - for reasons Richard can't + imagine and Kahlan dare not say. In their darkest hour, hunted + relentlessly, tormented by treachery and loss, Kahlan calls upon Richard + to reach beyond his sword - to invoke within himself something more + noble. Neither knows that the rules of battle have just changed...or + that their time has run out. Wizard's First Rule is the beginning. One + book. One Rule. Witness the birth of a legend. + isbn: + description: The ISBN of the book. Will be null if unknown. + type: string + nullable: true + asin: + description: The ASIN of the book. Will be null if unknown. + type: string + nullable: true + example: B002V0QK4C + language: + description: The language of the book. Will be null if unknown. + type: string + nullable: true + explicit: + description: Whether the book has been marked as explicit. + type: boolean + example: false + bookMetadataMinified: + type: object + description: The minified metadata for a book in the database. + allOf: + - $ref : '#/components/schemas/bookMetadataBase' + - type: object + properties: + titleIgnorePrefix: + description: The title of the book with any prefix moved to the end. + type: string + authorName: + description: The name of the book's author(s). + type: string + example: Terry Goodkind + authorNameLF: + description: The name of the book's author(s) with last names first. + type: string + example: Goodkind, Terry + narratorName: + description: The name of the audiobook's narrator(s). + type: string + example: Sam Tsoutsouvas + seriesName: + description: The name of the book's series. + type: string + example: Sword of Truth + bookChapter: + type: object + description: A book chapter. Includes the title and timestamps. + properties: + id: + description: The ID of the book chapter. + type: integer + example: 0 + start: + description: When in the book (in seconds) the chapter starts. + type: integer + example: 0 + end: + description: When in the book (in seconds) the chapter ends. + type: number + example: 6004.6675 + title: + description: The title of the chapter. + type: string + example: Wizards First Rule 01 Chapter 1 diff --git a/docs/objects/metadata/FileMetadata.yaml b/docs/objects/metadata/FileMetadata.yaml new file mode 100644 index 00000000..13e15e8b --- /dev/null +++ b/docs/objects/metadata/FileMetadata.yaml @@ -0,0 +1,39 @@ +components: + schemas: + fileMetadata: + type: object + description: The metadata for a file, including the path, size, and unix timestamps of the file. + nullable: true + properties: + filename: + description: The filename of the file. + type: string + example: Wizards First Rule 01.mp3 + ext: + description: The file extension of the file. + type: string + example: .mp3 + path: + description: The absolute path on the server of the file. + type: string + example: >- + /audiobooks/Terry Goodkind/Sword of Truth/Wizards First Rule/Terry + Goodkind - SOT Bk01 - Wizards First Rule 01.mp3 + relPath: + description: The path of the file, relative to the book's or podcast's folder. + type: string + example: Wizards First Rule 01.mp3 + size: + $ref: '../../schemas.yaml#/components/schemas/size' + mtimeMs: + description: The time (in ms since POSIX epoch) when the file was last modified on disk. + type: integer + example: 1632223180278 + ctimeMs: + description: The time (in ms since POSIX epoch) when the file status was changed on disk. + type: integer + example: 1645978261001 + birthtimeMs: + description: The time (in ms since POSIX epoch) when the file was created on disk. Will be 0 if unknown. + type: integer + example: 0 diff --git a/docs/schemas.yaml b/docs/schemas.yaml new file mode 100644 index 00000000..de506d46 --- /dev/null +++ b/docs/schemas.yaml @@ -0,0 +1,33 @@ +components: + schemas: + addedAt: + type: integer + description: The time (in ms since POSIX epoch) when added to the server. + example: 1633522963509 + createdAt: + type: integer + description: The time (in ms since POSIX epoch) when was created. + example: 1633522963509 + updatedAt: + type: integer + description: The time (in ms since POSIX epoch) when last updated. + example: 1633522963509 + size: + description: The total size (in bytes) of the item or file. + type: integer + example: 268824228 + durationSec: + description: The total length (in seconds) of the item or file. + type: number + example: 33854.905 + tags: + description: Tags applied to items. + type: array + items: + type: string + example: ["To Be Read", "Genre: Nonfiction"] + inode: + description: The inode of the item in the file system. + type: string + format: "[0-9]*" + example: '649644248522215260' \ No newline at end of file diff --git a/docs/spec.yaml b/docs/spec.yaml index dfe95b69..fdab0125 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -3,970 +3,149 @@ info: title: Audiobookshelf API version: 0.1.0 description: Audiobookshelf API with autogenerated OpenAPI doc - servers: - url: http://localhost:3000 description: Development server - components: - schemas: - mediaType: - type: string - description: The type of media, will be book or podcast. - enum: [book, podcast] - mediaMinified: - description: The minified media of the library item. - oneOf: - - $ref: '#/components/schemas/bookMinified' - bookCoverPath: - description: The absolute path on the server of the cover file. Will be null if there is no cover. - type: string - nullable: true - example: /audiobooks/Terry Goodkind/Sword of Truth/Wizards First Rule/cover.jpg - bookBase: - type: object - description: Base book schema - properties: - libraryItemId: - $ref: '#/components/schemas/libraryItemId' - coverPath: - $ref: '#/components/schemas/bookCoverPath' - tags: - $ref: '#/components/schemas/tags' - audioFiles: - type: array - items: - $ref: '#/components/schemas/audioFile' - chapters: - type: array - items: - $ref: '#/components/schemas/bookChapter' - missingParts: - description: Any parts missing from the book by track index. - type: array - items: - type: integer - ebookFile: - $ref: '#/components/schemas/ebookFile' - bookMinified: - type: object - description: Minified book schema. Does not depend on `bookBase` because there's pretty much no overlap. - properties: - metadata: - $ref: '#/components/schemas/bookMetadataMinified' - coverPath: - $ref: '#/components/schemas/bookCoverPath' - tags: - $ref: '#/components/schemas/tags' - numTracks: - description: The number of tracks the book's audio files have. - type: integer - example: 1 - numAudioFiles: - description: The number of audio files the book has. - type: integer - example: 1 - numChapters: - description: The number of chapters the book has. - type: integer - example: 1 - numMissingParts: - description: The total number of missing parts the book has. - type: integer - example: 0 - numInvalidAudioFiles: - description: The number of invalid audio files the book has. - type: integer - example: 0 - duration: - $ref: '#/components/schemas/durationSec' - size: - $ref: '#/components/schemas/size' - ebookFormat: - description: The format of ebook of the book. Will be null if the book is an audiobook. - type: string - nullable: true - narrators: - description: The narrators of the audiobook. - type: array - items: - type: string - example: Sam Tsoutsouvas - bookMetadataBase: - type: object - description: The base book metadata object for minified, normal, and extended schemas to inherit from. - properties: - title: - description: The title of the book. Will be null if unknown. - type: string - nullable: true - example: Wizards First Rule - subtitle: - description: The subtitle of the book. Will be null if there is no subtitle. - type: string - nullable: true - genres: - description: The genres of the book. - type: array - items: - type: string - example: ["Fantasy", "Sci-Fi", "Nonfiction: History"] - publishedYear: - description: The year the book was published. Will be null if unknown. - type: string - nullable: true - example: '2008' - publishedDate: - description: The date the book was published. Will be null if unknown. - type: string - nullable: true - publisher: - description: The publisher of the book. Will be null if unknown. - type: string - nullable: true - example: Brilliance Audio - description: - description: A description for the book. Will be null if empty. - type: string - nullable: true - example: >- - The masterpiece that started Terry Goodkind's New York Times bestselling - epic Sword of Truth In the aftermath of the brutal murder of his father, - a mysterious woman, Kahlan Amnell, appears in Richard Cypher's forest - sanctuary seeking help...and more. His world, his very beliefs, are - shattered when ancient debts come due with thundering violence. In a - dark age it takes courage to live, and more than mere courage to - challenge those who hold dominion, Richard and Kahlan must take up that - challenge or become the next victims. Beyond awaits a bewitching land - where even the best of their hearts could betray them. Yet, Richard - fears nothing so much as what secrets his sword might reveal about his - own soul. Falling in love would destroy them - for reasons Richard can't - imagine and Kahlan dare not say. In their darkest hour, hunted - relentlessly, tormented by treachery and loss, Kahlan calls upon Richard - to reach beyond his sword - to invoke within himself something more - noble. Neither knows that the rules of battle have just changed...or - that their time has run out. Wizard's First Rule is the beginning. One - book. One Rule. Witness the birth of a legend. - isbn: - description: The ISBN of the book. Will be null if unknown. - type: string - nullable: true - asin: - description: The ASIN of the book. Will be null if unknown. - type: string - nullable: true - example: B002V0QK4C - language: - description: The language of the book. Will be null if unknown. - type: string - nullable: true - explicit: - description: Whether the book has been marked as explicit. - type: boolean - example: false - bookMetadataMinified: - type: object - description: The minified metadata for a book in the database. - allOf: - - $ref : '#/components/schemas/bookMetadataBase' - - type: object - properties: - titleIgnorePrefix: - description: The title of the book with any prefix moved to the end. - type: string - authorName: - description: The name of the book's author(s). - type: string - example: Terry Goodkind - authorNameLF: - description: The name of the book's author(s) with last names first. - type: string - example: Goodkind, Terry - narratorName: - description: The name of the audiobook's narrator(s). - type: string - example: Sam Tsoutsouvas - seriesName: - description: The name of the book's series. - type: string - example: Sword of Truth - bookChapter: - type: object - description: A book chapter. Includes the title and timestamps. - properties: - id: - description: The ID of the book chapter. - type: integer - example: 0 - start: - description: When in the book (in seconds) the chapter starts. - type: integer - example: 0 - end: - description: When in the book (in seconds) the chapter ends. - type: number - example: 6004.6675 - title: - description: The title of the chapter. - type: string - example: Wizards First Rule 01 Chapter 1 - audioFile: - type: object - description: An audio file for a book. Includes audio metadata and track numbers. - properties: - index: - description: The index of the audio file. - type: integer - example: 1 - ino: - $ref: '#/components/schemas/inode' - metadata: - $ref: '#/components/schemas/fileMetadata' - addedAt: - $ref: '#/components/schemas/addedAt' - updatedAt: - $ref: '#/components/schemas/updatedAt' - trackNumFromMeta: - description: The track number of the audio file as pulled from the file's metadata. Will be null if unknown. - type: integer - nullable: true - example: 1 - discNumFromMeta: - description: The disc number of the audio file as pulled from the file's metadata. Will be null if unknown. - type: string - nullable: true - trackNumFromFilename: - description: The track number of the audio file as determined from the file's name. Will be null if unknown. - type: integer - nullable: true - example: 1 - discNumFromFilename: - description: The disc number of the audio file as determined from the file's name. Will be null if unknown. - type: string - nullable: true - manuallyVerified: - description: Whether the audio file has been manually verified by a user. - type: boolean - invalid: - description: Whether the audio file is missing from the server. - type: boolean - exclude: - description: Whether the audio file has been marked for exclusion. - type: boolean - error: - description: Any error with the audio file. Will be null if there is none. - type: string - nullable: true - format: - description: The format of the audio file. - type: string - example: MP2/3 (MPEG audio layer 2/3) - duration: - $ref: '#/components/schemas/durationSec' - bitRate: - description: The bit rate (in bit/s) of the audio file. - type: integer - example: 64000 - language: - description: The language of the audio file. - type: string - nullable: true - codec: - description: The codec of the audio file. - type: string - example: mp3 - timeBase: - description: The time base of the audio file. - type: string - example: 1/14112000 - channels: - description: The number of channels the audio file has. - type: integer - example: 2 - channelLayout: - description: The layout of the audio file's channels. - type: string - example: stereo - chapters: - description: If the audio file is part of an audiobook, the chapters the file contains. - type: array - items: - $ref: '#/components/schemas/bookChapter' - embeddedCoverArt: - description: The type of embedded cover art in the audio file. Will be null if none exists. - type: string - nullable: true - metaTags: - $ref: '#/components/schemas/audioMetaTags' - mimeType: - description: The MIME type of the audio file. - type: string - example: audio/mpeg - ebookFile: - type: object - description: An ebook file for a library item. - nullable: true - properties: - ino: - $ref: '#/components/schemas/inode' - metadata: - $ref: '#/components/schemas/fileMetadata' - ebookFormat: - description: The ebook format of the ebook file. - type: string - example: epub - addedAt: - $ref: '#/components/schemas/addedAt' - updatedAt: - $ref: '#/components/schemas/updatedAt' - fileMetadata: - type: object - description: The metadata for a file, including the path, size, and unix timestamps of the file. - nullable: true - properties: - filename: - description: The filename of the file. - type: string - example: Wizards First Rule 01.mp3 - ext: - description: The file extension of the file. - type: string - example: .mp3 - path: - description: The absolute path on the server of the file. - type: string - example: >- - /audiobooks/Terry Goodkind/Sword of Truth/Wizards First Rule/Terry - Goodkind - SOT Bk01 - Wizards First Rule 01.mp3 - relPath: - description: The path of the file, relative to the book's or podcast's folder. - type: string - example: Wizards First Rule 01.mp3 - size: - $ref: '#/components/schemas/size' - mtimeMs: - description: The time (in ms since POSIX epoch) when the file was last modified on disk. - type: integer - example: 1632223180278 - ctimeMs: - description: The time (in ms since POSIX epoch) when the file status was changed on disk. - type: integer - example: 1645978261001 - birthtimeMs: - description: The time (in ms since POSIX epoch) when the file was created on disk. Will be 0 if unknown. - type: integer - example: 0 - audioMetaTags: - description: ID3 metadata tags pulled from the audio file on import. Only non-null tags will be returned in requests. - type: object - properties: - tagAlbum: - type: string - nullable: true - example: SOT Bk01 - tagArtist: - type: string - nullable: true - example: Terry Goodkind - tagGenre: - type: string - nullable: true - example: Audiobook Fantasy - tagTitle: - type: string - nullable: true - example: Wizards First Rule 01 - tagSeries: - type: string - nullable: true - tagSeriesPart: - type: string - nullable: true - tagTrack: - type: string - nullable: true - example: 01/20 - tagDisc: - type: string - nullable: true - tagSubtitle: - type: string - nullable: true - tagAlbumArtist: - type: string - nullable: true - example: Terry Goodkind - tagDate: - type: string - nullable: true - tagComposer: - type: string - nullable: true - example: Terry Goodkind - tagPublisher: - type: string - nullable: true - tagComment: - type: string - nullable: true - tagDescription: - type: string - nullable: true - tagEncoder: - type: string - nullable: true - tagEncodedBy: - type: string - nullable: true - tagIsbn: - type: string - nullable: true - tagLanguage: - type: string - nullable: true - tagASIN: - type: string - nullable: true - tagOverdriveMediaMarker: - type: string - nullable: true - tagOriginalYear: - type: string - nullable: true - tagReleaseCountry: - type: string - nullable: true - tagReleaseType: - type: string - nullable: true - tagReleaseStatus: - type: string - nullable: true - tagISRC: - type: string - nullable: true - tagMusicBrainzTrackId: - type: string - nullable: true - tagMusicBrainzAlbumId: - type: string - nullable: true - tagMusicBrainzAlbumArtistId: - type: string - nullable: true - tagMusicBrainzArtistId: - type: string - nullable: true - addedAt: - type: integer - description: The time (in ms since POSIX epoch) when added to the server. - example: 1633522963509 - createdAt: - type: integer - description: The time (in ms since POSIX epoch) when was created. - example: 1633522963509 - updatedAt: - type: integer - description: The time (in ms since POSIX epoch) when last updated. - example: 1633522963509 - size: - description: The total size (in bytes) of the item or file. - type: integer - example: 268824228 - durationSec: - description: The total length (in seconds) of the item or file. - type: number - example: 33854.905 - tags: - description: Tags applied to items. - type: array - items: - type: string - example: ["To Be Read", "Genre: Nonfiction"] - inode: - description: The inode of the item in the file system. - type: string - format: "[0-9]*" - example: '649644248522215260' - seriesId: - type: string - description: The ID of the series. - format: uuid - example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b - seriesName: - description: The name of the series. - type: string - example: Sword of Truth - folderId: - type: string - description: The ID of the folder. - format: uuid - example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b - folder: - type: object - description: Folder used in library - properties: - id: - $ref: '#/components/schemas/folderId' - fullPath: - description: The path on the server for the folder. (Read Only) - type: string - example: /podcasts - libraryId: - $ref: '#/components/schemas/libraryId' - addedAt: - $ref: '#/components/schemas/addedAt' - authorUpdated: - description: Whether the author was updated without errors. Will not exist if author was merged. - type: boolean - nullable: true - authorId: - type: string - description: The ID of the author. - format: uuid - example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b - authorASIN: - type: string - description: The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier. - nullable: true - example: B000APZOQA - authorName: - description: The name of the author. - type: string - example: Terry Goodkind - authorSeries: - type: object - description: Series and the included library items that an author has written. - properties: - id: - $ref: '#/components/schemas/seriesId' - name: - $ref: '#/components/schemas/seriesName' - items: - description: The items in the series. Each library item's media's metadata will have a `series` attribute, a `Series Sequence`, which is the matching series. - type: array - items: - $ref: '#/components/schemas/libraryItemMinified' - author: - description: An author object which includes a description and image path. - type: object - properties: - id: - $ref: '#/components/schemas/authorId' - asin: - $ref: '#/components/schemas/authorASIN' - name: - $ref: '#/components/schemas/authorName' - description: - description: A description of the author. Will be null if there is none. - type: string - nullable: true - example: | - Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork, - ‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20 - languages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary - tour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. Terry Goodkind's - brilliant books are character-driven stories, with a focus on the complexity of the human psyche. Goodkind - has an uncanny grasp for crafting compelling stories about people like you and me, trapped in terrifying - situations. - imagePath: - description: The absolute path for the author image located in the `metadata/` directory. Will be null if there is no image. - type: string - nullable: true - example: /metadata/authors/aut_bxxbyjiptmgb56yzoz.jpg - addedAt: - $ref: '#/components/schemas/addedAt' - updatedAt: - $ref: '#/components/schemas/updatedAt' - authorWithItems: - type: object - description: The author schema with an array of items they are associated with. - allOf: - - $ref: '#/components/schemas/author' - - type: object - properties: - libraryItems: - description: The items associated with the author - type: array - items: - $ref: '#/components/schemas/libraryItemMinified' - authorWithSeries: - type: object - description: The author schema with an array of items and series they are associated with. - allOf: - - $ref: '#/components/schemas/authorWithItems' - - type: object - properties: - series: - description: The series associated with the author - type: array - items: - $ref: '#/components/schemas/authorSeries' - authorMinified: - type: object - description: Minified author object which only contains the author name and ID. - properties: - id: - $ref: '#/components/schemas/authorId' - name: - $ref: '#/components/schemas/authorName' - authorExpanded: - type: object - description: The author schema with the total number of books in the library. - allOf: - - $ref: '#/components/schemas/author' - - type: object - properties: - numBooks: - description: The number of books associated with the author in the library. - type: integer - example: 1 - oldLibraryId: - type: string - description: The ID of the libraries created on server version 2.2.23 and before. - format: "lib_[a-z0-9]{18}" - example: lib_o78uaoeuh78h6aoeif - libraryId: - type: string - description: The ID of the library. - format: uuid - example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b - oldLibraryItemId: - description: The ID of library items on server version 2.2.23 and before. - type: string - nullable: true - format: "li_[a-z0-9]{18}" - example: li_o78uaoeuh78h6aoeif - libraryItemId: - type: string - description: The ID of library items after 2.3.0. - format: uuid - example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b - libraryItemBase: - type: object - description: Base library item schema - properties: - id: - $ref: '#/components/schemas/libraryItemId' - oldLibraryItemId: - $ref: '#/components/schemas/oldLibraryItemId' - ino: - $ref: '#/components/schemas/inode' - libraryId: - $ref: '#/components/schemas/libraryId' - folderId: - $ref: '#/components/schemas/folderId' - path: - description: The path of the library item on the server. - type: string - relPath: - description: The path, relative to the library folder, of the library item. - type: string - isFile: - description: Whether the library item is a single file in the root of the library folder. - type: boolean - mtimeMs: - description: The time (in ms since POSIX epoch) when the library item was last modified on disk. - type: integer - ctimeMs: - description: The time (in ms since POSIX epoch) when the library item status was changed on disk. - type: integer - birthtimeMs: - description: The time (in ms since POSIX epoch) when the library item was created on disk. Will be 0 if unknown. - type: integer - addedAt: - $ref: '#/components/schemas/addedAt' - updatedAt: - $ref: '#/components/schemas/updatedAt' - isMissing: - description: Whether the library item was scanned and no longer exists. - type: boolean - isInvalid: - description: Whether the library item was scanned and no longer has media files. - type: boolean - mediaType: - $ref: '#/components/schemas/mediaType' - libraryItemMinified: - type: object - description: A single item on the server, like a book or podcast. Minified media format. - allOf: - - $ref : '#/components/schemas/libraryItemBase' - - type: object - properties: - media: - $ref: '#/components/schemas/mediaMinified' - parameters: - authorID: - name: id - in: path - description: Author ID - required: true - schema: - $ref: '#/components/schemas/authorId' - authorInclude: - name: include - in: query - description: A comma separated list of what to include with the author. The options are `items` and `series`. `series` will only have an effect if `items` is included. - required: false - schema: - type: string - example: "items" - examples: - empty: - summary: Do not return library items - value: "" - itemOnly: - summary: Only return library items - value: "items" - itemsAndSeries: - summary: Return library items and series - value: "items,series" - authorLibraryId: - name: library - in: query - description: The ID of the library to to include filter included items from. - required: false - schema: - $ref: '#/components/schemas/libraryId' - asin: - name: asin - in: query - description: The Audible Identifier (ASIN). - required: false - schema: - $ref: '#/components/schemas/authorASIN' - authorSearchName: - name: q - in: query - description: The name of the author to use for searching. - required: false - schema: - type: string - example: Terry Goodkind - authorName: - name: name - in: query - description: The new name of the author. - required: false - schema: - $ref: '#/components/schemas/authorName' - authorDescription: - name: description - in: query - description: The new description of the author. - required: false - schema: - type: string - nullable: true - example: Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork, ‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20 languages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary tour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. - authorImagePath: - name: imagePath - in: query - description: The new absolute path for the author image. - required: false - schema: - type: string - nullable: true - example: /metadata/authors/aut_z3leimgybl7uf3y4ab.jpg - imageURL: - name: url - in: query - description: The URL of the image to add to the server - required: true - schema: - type: string - format: uri - example: https://images-na.ssl-images-amazon.com/images/I/51NoQTm33OL.__01_SX120_CR0,0,120,120__.jpg - imageWidth: - name: width - in: query - description: The requested width of image in pixels. - schema: - type: integer - default: 400 - example: 400 - example: 400 - imageHeight: - name: height - in: query - description: The requested height of image in pixels. If `null`, the height is scaled to maintain aspect ratio based on the requested width. - schema: - type: integer - nullable: true - default: null - example: 600 - examples: - scaleHeight: - summary: Scale height with width - value: null - fixedHeight: - summary: Force height of image - value: 600 - imageFormat: - name: format - in: query - description: The requested output format. - schema: - type: string - default: jpeg - example: webp - imageRaw: - name: raw - in: query - description: Return the raw image without scaling if true. - schema: - type: boolean - default: false responses: ok200: description: OK - author404: - description: Author not found. - content: - text/html: - schema: - type: string - example: Not found - paths: - /api/authors/{id}: - get: - operationId: getAuthorByID - summary: Get a single author by ID on server - tags: - - Authors - parameters: - - $ref: '#/components/parameters/authorID' - - $ref: '#/components/parameters/authorInclude' - - $ref: '#/components/parameters/authorLibraryId' - responses: - 200: - description: getAuthorByID OK - content: - application/json: - schema: - oneOf: - - $ref: '#/components/schemas/author' - - $ref: '#/components/schemas/authorWithItems' - - $ref: '#/components/schemas/authorWithSeries' - 404: - $ref: '#/components/responses/author404' - /api/authors/{id}: - patch: - operationId: updateAuthorByID - summary: Update a single author by ID on server. This endpoint will merge two authors if the new author name matches another author in the database. - tags: - - Authors - parameters: - - $ref: '#/components/parameters/authorID' - - $ref: '#/components/parameters/asin' - - $ref: '#/components/parameters/authorName' - - $ref: '#/components/parameters/authorDescription' - - $ref: '#/components/parameters/authorImagePath' - responses: - 200: - description: updateAuthorByID OK - content: - application/json: - schema: - allOf: - - $ref: '#/components/schemas/author' - - $ref: '#/components/schemas/authorUpdated' - - type: object - properties: - merged: - description: Will only exist and be `true` if the author was merged with another author - type: boolean - nullable: true - 404: - $ref: '#/components/responses/author404' - /api/authors/{id}: - delete: - operationId: deleteAuthorByID - summary: Delete a single author by ID on server and remove author from all books. - tags: - - Authors - parameters: - - $ref: '#/components/parameters/authorID' - responses: - 200: - $ref: '#/components/responses/ok200' - 404: - $ref: '#/components/responses/author404' - /api/authors/{id}/image: - post: - operationId: setAuthorImageByID - summary: Set an author image using a provided URL. - tags: - - Authors - parameters: - - $ref: '#/components/parameters/authorID' - - $ref: '#/components/parameters/imageURL' - responses: - 200: - description: setAuthorImageByID OK - content: - application/json: - schema: - oneOf: - - $ref: '#/components/schemas/author' - 404: - $ref: '#/components/responses/author404' - /api/authors/{id}/image: - delete: - operationId: deleteAuthorImageByID - summary: Delete an author image from the server and remove the image from the database. - tags: - - Authors - parameters: - - $ref: '#/components/parameters/authorID' - responses: - 200: - $ref: '#/components/responses/ok200' - 404: - $ref: '#/components/responses/author404' - /api/authors/{id}/match: - post: - operationId: matchAuthorByID - summary: Match the author against Audible using quick match. Quick match updates the author's description and image (if no image already existed) with information from audible. Either `asin` or `q` must be provided, with `asin` taking priority if both are provided. - tags: - - Authors - parameters: - - $ref: '#/components/parameters/authorID' - - $ref: '#/components/parameters/asin' - - $ref: '#/components/parameters/authorSearchName' - responses: - 200: - description: matchAuthorByID OK - content: - application/json: - schema: - allOf: - - $ref: '#/components/schemas/author' - - $ref: '#/components/schemas/authorUpdated' - 404: - $ref: '#/components/responses/author404' - /api/authors/{id}/image: - patch: - operationId: getAuthorImageByID - summary: Return the author image by author ID. - tags: - - Authors - parameters: - - $ref: '#/components/parameters/authorID' - - $ref: '#/components/parameters/imageWidth' - - $ref: '#/components/parameters/imageHeight' - - $ref: '#/components/parameters/imageFormat' - - $ref: '#/components/parameters/imageRaw' - responses: - 200: - description: getAuthorImageByID OK - content: - image/*: - schema: - type: string - format: binary - 404: - $ref: '#/components/responses/author404' +paths: + /api/authors/{id}: + get: + operationId: getAuthorByID + summary: Get a single author by ID on server + tags: + - Authors + parameters: + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorID' + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorInclude' + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorLibraryId' + responses: + 200: + description: getAuthorByID OK + content: + application/json: + schema: + oneOf: + - $ref: './objects/entities/Author.yaml#/components/schemas/author' + - $ref: './objects/entities/Author.yaml#/components/schemas/authorWithItems' + - $ref: './objects/entities/Author.yaml#/components/schemas/authorWithSeries' + 404: + $ref: './controllers/AuthorController.yaml#/components/responses/author404' + patch: + operationId: updateAuthorByID + summary: Update a single author by ID on server. This endpoint will merge two authors if the new author name matches another author in the database. + tags: + - Authors + parameters: + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorID' + - $ref: './controllers/AuthorController.yaml#/components/parameters/asin' + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorName' + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorDescription' + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorImagePath' + responses: + 200: + description: updateAuthorByID OK + content: + application/json: + schema: + allOf: + - $ref: './objects/entities/Author.yaml#/components/schemas/author' + - $ref: './controllers/AuthorController.yaml#/components/schemas/authorUpdated' + - type: object + properties: + merged: + description: Will only exist and be `true` if the author was merged with another author + type: boolean + nullable: true + 404: + $ref: './controllers/AuthorController.yaml#/components/responses/author404' + delete: + operationId: deleteAuthorByID + summary: Delete a single author by ID on server and remove author from all books. + tags: + - Authors + parameters: + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorID' + responses: + 200: + $ref: '#/components/responses/ok200' + 404: + $ref: './controllers/AuthorController.yaml#/components/responses/author404' + /api/authors/{id}/image: + post: + operationId: setAuthorImageByID + summary: Set an author image using a provided URL. + tags: + - Authors + parameters: + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorID' + - $ref: './controllers/AuthorController.yaml#/components/parameters/imageURL' + responses: + 200: + description: setAuthorImageByID OK + content: + application/json: + schema: + oneOf: + - $ref: './objects/entities/Author.yaml#/components/schemas/author' + 404: + $ref: './controllers/AuthorController.yaml#/components/responses/author404' + delete: + operationId: deleteAuthorImageByID + summary: Delete an author image from the server and remove the image from the database. + tags: + - Authors + parameters: + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorID' + responses: + 200: + $ref: '#/components/responses/ok200' + 404: + $ref: './controllers/AuthorController.yaml#/components/responses/author404' + patch: + operationId: getAuthorImageByID + summary: Return the author image by author ID. + tags: + - Authors + parameters: + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorID' + - $ref: './controllers/AuthorController.yaml#/components/parameters/imageWidth' + - $ref: './controllers/AuthorController.yaml#/components/parameters/imageHeight' + - $ref: './controllers/AuthorController.yaml#/components/parameters/imageFormat' + - $ref: './controllers/AuthorController.yaml#/components/parameters/imageRaw' + responses: + 200: + description: getAuthorImageByID OK + content: + image/*: + schema: + type: string + format: binary + 404: + $ref: './controllers/AuthorController.yaml#/components/responses/author404' + /api/authors/{id}/match: + post: + operationId: matchAuthorByID + summary: Match the author against Audible using quick match. Quick match updates the author's description and image (if no image already existed) with information from audible. Either `asin` or `q` must be provided, with `asin` taking priority if both are provided. + tags: + - Authors + parameters: + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorID' + - $ref: './controllers/AuthorController.yaml#/components/parameters/asin' + - $ref: './controllers/AuthorController.yaml#/components/parameters/authorSearchName' + responses: + 200: + description: matchAuthorByID OK + content: + application/json: + schema: + allOf: + - $ref: './objects/entities/Author.yaml#/components/schemas/author' + - $ref: './controllers/AuthorController.yaml#/components/schemas/authorUpdated' + 404: + $ref: './controllers/AuthorController.yaml#/components/responses/author404' tags: - name: Authors description: Author endpoints From 7b856474afcf33cae2c0c9adccd1fa3fd1026593 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Sun, 31 Mar 2024 22:48:58 +0000 Subject: [PATCH 0498/2145] Rename base document --- docs/{spec.yaml => root.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{spec.yaml => root.yaml} (100%) diff --git a/docs/spec.yaml b/docs/root.yaml similarity index 100% rename from docs/spec.yaml rename to docs/root.yaml From 74dd24febf216edd9a9eda2f7778d943d39fbaf5 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Mon, 1 Apr 2024 00:26:55 +0000 Subject: [PATCH 0499/2145] Bundled spec --- docs/openapi.json | 856 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 856 insertions(+) create mode 100644 docs/openapi.json diff --git a/docs/openapi.json b/docs/openapi.json new file mode 100644 index 00000000..8b47584b --- /dev/null +++ b/docs/openapi.json @@ -0,0 +1,856 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Audiobookshelf API", + "version": "0.1.0", + "description": "Audiobookshelf API with autogenerated OpenAPI doc" + }, + "servers": [ + { + "url": "http://localhost:3000", + "description": "Development server" + } + ], + "components": { + "responses": { + "ok200": { + "description": "OK" + } + } + }, + "paths": { + "/api/authors/{id}": { + "get": { + "operationId": "getAuthorByID", + "summary": "Get a single author by ID on server", + "tags": [ + "Authors" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Author ID", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + } + }, + { + "name": "include", + "in": "query", + "description": "A comma separated list of what to include with the author. The options are `items` and `series`. `series` will only have an effect if `items` is included.", + "required": false, + "schema": { + "type": "string", + "example": "items" + }, + "examples": { + "empty": { + "summary": "Do not return library items", + "value": "" + }, + "itemOnly": { + "summary": "Only return library items", + "value": "items" + }, + "itemsAndSeries": { + "summary": "Return library items and series", + "value": "items,series" + } + } + }, + { + "name": "library", + "in": "query", + "description": "The ID of the library to to include filter included items from.", + "required": false, + "schema": { + "type": "string", + "description": "The ID of the library.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + } + } + ], + "responses": { + "200": { + "description": "getAuthorByID OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "description": "An author object which includes a description and image path.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + }, + "asin": { + "type": "string", + "description": "The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier.", + "nullable": true, + "example": "B000APZOQA" + }, + "name": { + "description": "The name of the author.", + "type": "string", + "example": "Terry Goodkind" + }, + "description": { + "description": "A description of the author. Will be null if there is none.", + "type": "string", + "nullable": true, + "example": "Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork,\n‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20\nlanguages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary\ntour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. Terry Goodkind's\nbrilliant books are character-driven stories, with a focus on the complexity of the human psyche. Goodkind\nhas an uncanny grasp for crafting compelling stories about people like you and me, trapped in terrifying\nsituations.\n" + }, + "imagePath": { + "description": "The absolute path for the author image located in the `metadata/` directory. Will be null if there is no image.", + "type": "string", + "nullable": true, + "example": "/metadata/authors/aut_bxxbyjiptmgb56yzoz.jpg" + }, + "addedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when added to the server.", + "example": 1633522963509 + }, + "updatedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when last updated.", + "example": 1633522963509 + } + } + }, + { + "type": "object", + "description": "The author schema with an array of items they are associated with.", + "allOf": [ + { + "description": "An author object which includes a description and image path.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + }, + "asin": { + "type": "string", + "description": "The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier.", + "nullable": true, + "example": "B000APZOQA" + }, + "name": { + "description": "The name of the author.", + "type": "string", + "example": "Terry Goodkind" + }, + "description": { + "description": "A description of the author. Will be null if there is none.", + "type": "string", + "nullable": true, + "example": "Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork,\n‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20\nlanguages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary\ntour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. Terry Goodkind's\nbrilliant books are character-driven stories, with a focus on the complexity of the human psyche. Goodkind\nhas an uncanny grasp for crafting compelling stories about people like you and me, trapped in terrifying\nsituations.\n" + }, + "imagePath": { + "description": "The absolute path for the author image located in the `metadata/` directory. Will be null if there is no image.", + "type": "string", + "nullable": true, + "example": "/metadata/authors/aut_bxxbyjiptmgb56yzoz.jpg" + }, + "addedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when added to the server.", + "example": 1633522963509 + }, + "updatedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when last updated.", + "example": 1633522963509 + } + } + }, + { + "type": "object", + "properties": { + "libraryItems": { + "description": "The items associated with the author", + "type": "string" + } + } + } + ] + }, + { + "type": "object", + "description": "The author schema with an array of items and series they are associated with.", + "allOf": [ + { + "type": "object", + "description": "The author schema with an array of items they are associated with.", + "allOf": [ + { + "description": "An author object which includes a description and image path.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + }, + "asin": { + "type": "string", + "description": "The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier.", + "nullable": true, + "example": "B000APZOQA" + }, + "name": { + "description": "The name of the author.", + "type": "string", + "example": "Terry Goodkind" + }, + "description": { + "description": "A description of the author. Will be null if there is none.", + "type": "string", + "nullable": true, + "example": "Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork,\n‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20\nlanguages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary\ntour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. Terry Goodkind's\nbrilliant books are character-driven stories, with a focus on the complexity of the human psyche. Goodkind\nhas an uncanny grasp for crafting compelling stories about people like you and me, trapped in terrifying\nsituations.\n" + }, + "imagePath": { + "description": "The absolute path for the author image located in the `metadata/` directory. Will be null if there is no image.", + "type": "string", + "nullable": true, + "example": "/metadata/authors/aut_bxxbyjiptmgb56yzoz.jpg" + }, + "addedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when added to the server.", + "example": 1633522963509 + }, + "updatedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when last updated.", + "example": 1633522963509 + } + } + }, + { + "type": "object", + "properties": { + "libraryItems": { + "description": "The items associated with the author", + "type": "string" + } + } + } + ] + }, + { + "type": "object", + "properties": { + "series": { + "description": "The series associated with the author", + "type": "array", + "items": { + "type": "object", + "description": "Series and the included library items that an author has written.", + "properties": { + "id": { + "type": "string", + "description": "The ID of the series.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + }, + "name": { + "description": "The name of the series.", + "type": "string", + "example": "Sword of Truth" + }, + "items": { + "description": "The items in the series. Each library item's media's metadata will have a `series` attribute, a `Series Sequence`, which is the matching series.", + "type": "array", + "items": {} + } + } + } + } + } + } + ] + } + ] + } + } + } + }, + "404": { + "description": "Author not found.", + "content": { + "text/html": { + "schema": { + "type": "string", + "example": "Not found" + } + } + } + } + } + }, + "patch": { + "operationId": "updateAuthorByID", + "summary": "Update a single author by ID on server. This endpoint will merge two authors if the new author name matches another author in the database.", + "tags": [ + "Authors" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Author ID", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + } + }, + { + "name": "asin", + "in": "query", + "description": "The Audible Identifier (ASIN).", + "required": false, + "schema": { + "type": "string", + "description": "The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier.", + "nullable": true, + "example": "B000APZOQA" + } + }, + { + "name": "name", + "in": "query", + "description": "The new name of the author.", + "required": false, + "schema": { + "description": "The name of the author.", + "type": "string", + "example": "Terry Goodkind" + } + }, + { + "name": "description", + "in": "query", + "description": "The new description of the author.", + "required": false, + "schema": { + "type": "string", + "nullable": true, + "example": "Terry Goodkind is a" + } + }, + { + "name": "imagePath", + "in": "query", + "description": "The new absolute path for the author image.", + "required": false, + "schema": { + "type": "string", + "nullable": true, + "example": "/metadata/authors/aut_z3leimgybl7uf3y4ab.jpg" + } + } + ], + "responses": { + "200": { + "description": "updateAuthorByID OK", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "description": "An author object which includes a description and image path.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + }, + "asin": { + "type": "string", + "description": "The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier.", + "nullable": true, + "example": "B000APZOQA" + }, + "name": { + "description": "The name of the author.", + "type": "string", + "example": "Terry Goodkind" + }, + "description": { + "description": "A description of the author. Will be null if there is none.", + "type": "string", + "nullable": true, + "example": "Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork,\n‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20\nlanguages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary\ntour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. Terry Goodkind's\nbrilliant books are character-driven stories, with a focus on the complexity of the human psyche. Goodkind\nhas an uncanny grasp for crafting compelling stories about people like you and me, trapped in terrifying\nsituations.\n" + }, + "imagePath": { + "description": "The absolute path for the author image located in the `metadata/` directory. Will be null if there is no image.", + "type": "string", + "nullable": true, + "example": "/metadata/authors/aut_bxxbyjiptmgb56yzoz.jpg" + }, + "addedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when added to the server.", + "example": 1633522963509 + }, + "updatedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when last updated.", + "example": 1633522963509 + } + } + }, + { + "description": "Whether the author was updated without errors. Will not exist if author was merged.", + "type": "boolean", + "nullable": true + }, + { + "type": "object", + "properties": { + "merged": { + "description": "Will only exist and be `true` if the author was merged with another author", + "type": "boolean", + "nullable": true + } + } + } + ] + } + } + } + }, + "404": { + "description": "Author not found.", + "content": { + "text/html": { + "schema": { + "type": "string", + "example": "Not found" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteAuthorByID", + "summary": "Delete a single author by ID on server and remove author from all books.", + "tags": [ + "Authors" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Author ID", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/ok200" + }, + "404": { + "description": "Author not found.", + "content": { + "text/html": { + "schema": { + "type": "string", + "example": "Not found" + } + } + } + } + } + } + }, + "/api/authors/{id}/image": { + "post": { + "operationId": "setAuthorImageByID", + "summary": "Set an author image using a provided URL.", + "tags": [ + "Authors" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Author ID", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + } + }, + { + "name": "url", + "in": "query", + "description": "The URL of the image to add to the server", + "required": true, + "schema": { + "type": "string", + "format": "uri", + "example": "https://images-na.ssl-images-amazon.com/images/I/51NoQTm33OL.__01_SX120_CR0,0,120,120__.jpg" + } + } + ], + "responses": { + "200": { + "description": "setAuthorImageByID OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "description": "An author object which includes a description and image path.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + }, + "asin": { + "type": "string", + "description": "The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier.", + "nullable": true, + "example": "B000APZOQA" + }, + "name": { + "description": "The name of the author.", + "type": "string", + "example": "Terry Goodkind" + }, + "description": { + "description": "A description of the author. Will be null if there is none.", + "type": "string", + "nullable": true, + "example": "Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork,\n‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20\nlanguages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary\ntour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. Terry Goodkind's\nbrilliant books are character-driven stories, with a focus on the complexity of the human psyche. Goodkind\nhas an uncanny grasp for crafting compelling stories about people like you and me, trapped in terrifying\nsituations.\n" + }, + "imagePath": { + "description": "The absolute path for the author image located in the `metadata/` directory. Will be null if there is no image.", + "type": "string", + "nullable": true, + "example": "/metadata/authors/aut_bxxbyjiptmgb56yzoz.jpg" + }, + "addedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when added to the server.", + "example": 1633522963509 + }, + "updatedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when last updated.", + "example": 1633522963509 + } + } + } + ] + } + } + } + }, + "404": { + "description": "Author not found.", + "content": { + "text/html": { + "schema": { + "type": "string", + "example": "Not found" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteAuthorImageByID", + "summary": "Delete an author image from the server and remove the image from the database.", + "tags": [ + "Authors" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Author ID", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/ok200" + }, + "404": { + "description": "Author not found.", + "content": { + "text/html": { + "schema": { + "type": "string", + "example": "Not found" + } + } + } + } + } + }, + "patch": { + "operationId": "getAuthorImageByID", + "summary": "Return the author image by author ID.", + "tags": [ + "Authors" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Author ID", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + } + }, + { + "name": "width", + "in": "query", + "description": "The requested width of image in pixels.", + "schema": { + "type": "integer", + "default": 400, + "example": 400 + }, + "example": 400 + }, + { + "name": "height", + "in": "query", + "description": "The requested height of image in pixels. If `null`, the height is scaled to maintain aspect ratio based on the requested width.", + "schema": { + "type": "integer", + "nullable": true, + "default": null, + "example": 600 + }, + "examples": { + "scaleHeight": { + "summary": "Scale height with width", + "value": null + }, + "fixedHeight": { + "summary": "Force height of image", + "value": 600 + } + } + }, + { + "name": "format", + "in": "query", + "description": "The requested output format.", + "schema": { + "type": "string", + "default": "jpeg", + "example": "webp" + } + }, + { + "name": "raw", + "in": "query", + "description": "Return the raw image without scaling if true.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "getAuthorImageByID OK", + "content": { + "image/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Author not found.", + "content": { + "text/html": { + "schema": { + "type": "string", + "example": "Not found" + } + } + } + } + } + } + }, + "/api/authors/{id}/match": { + "post": { + "operationId": "matchAuthorByID", + "summary": "Match the author against Audible using quick match. Quick match updates the author's description and image (if no image already existed) with information from audible. Either `asin` or `q` must be provided, with `asin` taking priority if both are provided.", + "tags": [ + "Authors" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Author ID", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + } + }, + { + "name": "asin", + "in": "query", + "description": "The Audible Identifier (ASIN).", + "required": false, + "schema": { + "type": "string", + "description": "The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier.", + "nullable": true, + "example": "B000APZOQA" + } + }, + { + "name": "q", + "in": "query", + "description": "The name of the author to use for searching.", + "required": false, + "schema": { + "type": "string", + "example": "Terry Goodkind" + } + } + ], + "responses": { + "200": { + "description": "matchAuthorByID OK", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "description": "An author object which includes a description and image path.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the author.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + }, + "asin": { + "type": "string", + "description": "The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier.", + "nullable": true, + "example": "B000APZOQA" + }, + "name": { + "description": "The name of the author.", + "type": "string", + "example": "Terry Goodkind" + }, + "description": { + "description": "A description of the author. Will be null if there is none.", + "type": "string", + "nullable": true, + "example": "Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork,\n‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20\nlanguages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary\ntour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. Terry Goodkind's\nbrilliant books are character-driven stories, with a focus on the complexity of the human psyche. Goodkind\nhas an uncanny grasp for crafting compelling stories about people like you and me, trapped in terrifying\nsituations.\n" + }, + "imagePath": { + "description": "The absolute path for the author image located in the `metadata/` directory. Will be null if there is no image.", + "type": "string", + "nullable": true, + "example": "/metadata/authors/aut_bxxbyjiptmgb56yzoz.jpg" + }, + "addedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when added to the server.", + "example": 1633522963509 + }, + "updatedAt": { + "type": "integer", + "description": "The time (in ms since POSIX epoch) when last updated.", + "example": 1633522963509 + } + } + }, + { + "description": "Whether the author was updated without errors. Will not exist if author was merged.", + "type": "boolean", + "nullable": true + } + ] + } + } + } + }, + "404": { + "description": "Author not found.", + "content": { + "text/html": { + "schema": { + "type": "string", + "example": "Not found" + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "Authors", + "description": "Author endpoints" + } + ] +} From ca7eaf97502676f0933925ad9450396f0c18161f Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Mon, 1 Apr 2024 00:44:51 +0000 Subject: [PATCH 0500/2145] OpenAPI spec readme --- docs/README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..c6f278ed --- /dev/null +++ b/docs/README.md @@ -0,0 +1,30 @@ +# OpenAPI specification + +This directory includes the OpenAPI spec for the ABS server. +The spec is made up of a number of individual `yaml` files located here and in the subfolders, with `root.yaml` being the file that references all of the others. +The files are organized to have the same hierarchy as the server source files. +The full spec is bundled into one file in `openapi.json`. + +The spec is linted and bundled by the [`vacuum` tool](https://quobix.com/vacuum/). +The spec can also be tested with a real server using the [`wiretap` tool](https://pb33f.io/wiretap/). +Both of these tools are created by [pb33f](https://pb33f.io/). + +### Bundling the spec +The command to bundle the spec into a `yaml` file is `vacuum bundle root.yaml openapi.yaml`. + +The current version of `vacuum` cannot convert input `yaml` files to `json` files. +To convert the spec to `json`, you can use the `yq` tool or another tool. + +The command to convert the spec using `yq` is `yq -p yaml -o json openapi.yaml > openapi.json`. + +### Viewing report +To generate an HTML report, you can use `vacuum html-report [file]` to generate `report.html` and view the report in your browser. + +### Putting it all together +The full command that I run to bundle the spec and generate the report is: + +``` +vacuum bundle root.yaml openapi.yaml && \ +yq -p yaml -o json openapi.yaml > openapi.json && \ +vacuum html-report openapi.json +``` From c8957fe373e97f33e08fe778a09b99811cf13e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rasmus=20Kr=C3=A4mer?= <git@rfk.io> Date: Mon, 1 Apr 2024 16:20:09 +0200 Subject: [PATCH 0501/2145] Add client name to possible device info lines --- client/pages/config/sessions.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index 162c7747..0437cfd2 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -396,6 +396,7 @@ export default { var lines = [] if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`) if (deviceInfo.browserName) lines.push(deviceInfo.browserName) + if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion}`) if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`) if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`) From 8e46181ba014be1a5249f84f7279c652cfd2db98 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 2 Apr 2024 18:05:44 -0500 Subject: [PATCH 0502/2145] Update:Adding tooltips to player controls forward/backward and next/prev #2800 --- client/assets/fonts.css | 1 + .../player/PlayerPlaybackControls.vue | 34 ++++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/client/assets/fonts.css b/client/assets/fonts.css index f9acc6bf..9504d4c3 100644 --- a/client/assets/fonts.css +++ b/client/assets/fonts.css @@ -24,6 +24,7 @@ word-wrap: normal; direction: ltr; -webkit-font-smoothing: antialiased; + vertical-align: top; } .material-icons:not([class*="text-"]) { diff --git a/client/components/player/PlayerPlaybackControls.vue b/client/components/player/PlayerPlaybackControls.vue index 91bc3be3..f3eabab0 100644 --- a/client/components/player/PlayerPlaybackControls.vue +++ b/client/components/player/PlayerPlaybackControls.vue @@ -1,22 +1,30 @@ <template> - <div class="flex pt-4 pb-2 md:pt-0 md:pb-2"> + <div class="flex items-center pt-4 pb-2 md:pt-0 md:pb-2"> <div class="flex-grow" /> <template v-if="!loading"> - <button :aria-label="$strings.ButtonPreviousChapter" class="flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter"> - <span class="material-icons text-2xl sm:text-3xl">first_page</span> - </button> - <button :aria-label="$strings.ButtonJumpBackward" class="flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward"> - <span class="material-icons text-2xl sm:text-3xl">replay_10</span> - </button> + <ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 md:mr-8"> + <button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter"> + <span class="material-icons text-2xl sm:text-3xl">first_page</span> + </button> + </ui-tooltip> + <ui-tooltip direction="top" :text="$strings.ButtonJumpBackward"> + <button :aria-label="$strings.ButtonJumpBackward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward"> + <span class="material-icons text-2xl sm:text-3xl">replay_10</span> + </button> + </ui-tooltip> <button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause"> <span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span> </button> - <button :aria-label="$strings.ButtonJumpForward" class="flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward"> - <span class="material-icons text-2xl sm:text-3xl">forward_10</span> - </button> - <button :aria-label="$strings.ButtonNextChapter" class="flex items-center justify-center ml-4 md:ml-8" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter"> - <span class="material-icons text-2xl sm:text-3xl">last_page</span> - </button> + <ui-tooltip direction="top" :text="$strings.ButtonJumpForward"> + <button :aria-label="$strings.ButtonJumpForward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward"> + <span class="material-icons text-2xl sm:text-3xl">forward_10</span> + </button> + </ui-tooltip> + <ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 md:ml-8"> + <button :aria-label="$strings.ButtonNextChapter" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter"> + <span class="material-icons text-2xl sm:text-3xl">last_page</span> + </button> + </ui-tooltip> <controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" /> </template> <template v-else> From 116a7fb994e156755053c43a8be1d7356bf3e786 Mon Sep 17 00:00:00 2001 From: SunX <yearnsun@gmail.com> Date: Wed, 3 Apr 2024 09:55:33 +0800 Subject: [PATCH 0503/2145] Update zh-cn.json --- client/strings/zh-cn.json | 60 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 69b03fb3..4ff8d74b 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -32,8 +32,8 @@ "ButtonHide": "隐藏", "ButtonHome": "首页", "ButtonIssues": "问题", - "ButtonJumpBackward": "Jump Backward", - "ButtonJumpForward": "Jump Forward", + "ButtonJumpBackward": "向后跳转", + "ButtonJumpForward": "向前跳转", "ButtonLatest": "最新", "ButtonLibrary": "媒体库", "ButtonLogout": "注销", @@ -43,8 +43,8 @@ "ButtonMatchAllAuthors": "匹配所有作者", "ButtonMatchBooks": "匹配图书", "ButtonNevermind": "没有关系", - "ButtonNext": "Next", - "ButtonNextChapter": "Next Chapter", + "ButtonNext": "下一个", + "ButtonNextChapter": "下一章节", "ButtonOk": "确定", "ButtonOpenFeed": "打开源", "ButtonOpenManager": "打开管理器", @@ -52,8 +52,8 @@ "ButtonPlay": "播放", "ButtonPlaying": "正在播放", "ButtonPlaylists": "播放列表", - "ButtonPrevious": "Previous", - "ButtonPreviousChapter": "Previous Chapter", + "ButtonPrevious": "上一个", + "ButtonPreviousChapter": "上一章节", "ButtonPurgeAllCache": "清理所有缓存", "ButtonPurgeItemsCache": "清理项目缓存", "ButtonPurgeMediaProgress": "清理媒体进度", @@ -61,7 +61,7 @@ "ButtonQueueRemoveItem": "从队列中移除", "ButtonQuickMatch": "快速匹配", "ButtonRead": "读取", - "ButtonRefresh": "Refresh", + "ButtonRefresh": "刷新", "ButtonRemove": "移除", "ButtonRemoveAll": "移除所有", "ButtonRemoveAllLibraryItems": "移除所有媒体库项目", @@ -113,7 +113,7 @@ "HeaderCollectionItems": "收藏项目", "HeaderCover": "封面", "HeaderCurrentDownloads": "当前下载", - "HeaderCustomMetadataProviders": "Custom Metadata Providers", + "HeaderCustomMetadataProviders": "自定义元数据提供者", "HeaderDetails": "详情", "HeaderDownloadQueue": "下载队列", "HeaderEbookFiles": "电子书文件", @@ -184,7 +184,7 @@ "HeaderUpdateDetails": "更新详情", "HeaderUpdateLibrary": "更新媒体库", "HeaderUsers": "用户", - "HeaderYearReview": "Year {0} in Review", + "HeaderYearReview": "{0} 年回顾", "HeaderYourStats": "你的统计数据", "LabelAbridged": "概要", "LabelAccountType": "帐户类型", @@ -294,9 +294,9 @@ "LabelFolders": "文件夹", "LabelFontBold": "Bold", "LabelFontFamily": "字体系列", - "LabelFontItalic": "Italic", + "LabelFontItalic": "斜体", "LabelFontScale": "字体比例", - "LabelFontStrikethrough": "Strikethrough", + "LabelFontStrikethrough": "删除线", "LabelFormat": "编码格式", "LabelGenre": "流派", "LabelGenres": "流派", @@ -355,8 +355,8 @@ "LabelMetaTags": "元标签", "LabelMinute": "分钟", "LabelMissing": "丢失", - "LabelMissingEbook": "Has no ebook", - "LabelMissingSupplementaryEbook": "Has no supplementary ebook", + "LabelMissingEbook": "没有电子书", + "LabelMissingSupplementaryEbook": "没有补充电子书", "LabelMobileRedirectURIs": "允许移动应用重定向 URI", "LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 <code>audiobookshelf://oauth</code>,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (<code>*</code>) 作为唯一条目允许任何 URI.", "LabelMore": "更多", @@ -385,9 +385,9 @@ "LabelNotStarted": "未开始", "LabelNumberOfBooks": "图书数量", "LabelNumberOfEpisodes": "# 集", - "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", - "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", - "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", + "LabelOpenIDAdvancedPermsClaimDescription": "OpenID 声明的名称, 该声明包含应用程序内用户操作的高级权限, 该权限将应用于非管理员角色(<b>如果已配置</b>). 如果响应中缺少声明, 获取 ABS 的权限将被拒绝. 如果缺少单个选项, 它将被视为 <code>禁用</code>. 确保身份提供者的声明与预期结构匹配:", + "LabelOpenIDClaims": "将以下选项留空以禁用高级组和权限分配, 然后自动分配 'User' 组.", + "LabelOpenIDGroupClaimDescription": "OpenID 声明的名称, 该声明包含用户组的列表. 通常称为<code>组</code><b>如果已配置</b>, 应用程序将根据用户的组成员身份自动分配角色, 前提是这些组在声明中以不区分大小写的方式命名为 'Admin', 'User' 或 'Guest'. 声明应包含一个列表, 如果用户属于多个组, 则应用程序将分配与最高访问级别相对应的角色. 如果没有组匹配, 访问将被拒绝.", "LabelOpenRSSFeed": "打开 RSS 源", "LabelOverwrite": "覆盖", "LabelPassword": "密码", @@ -399,7 +399,7 @@ "LabelPermissionsDownload": "可以下载", "LabelPermissionsUpdate": "可以更新", "LabelPermissionsUpload": "可以上传", - "LabelPersonalYearReview": "Your Year in Review ({0})", + "LabelPersonalYearReview": "你的年度回顾 ({0})", "LabelPhotoPathURL": "图片路径或 URL", "LabelPlaylists": "播放列表", "LabelPlayMethod": "播放方法", @@ -426,7 +426,7 @@ "LabelRegion": "区域", "LabelReleaseDate": "发布日期", "LabelRemoveCover": "移除封面", - "LabelRowsPerPage": "Rows per page", + "LabelRowsPerPage": "每页行数", "LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件", "LabelRSSFeedCustomOwnerName": "自定义所有者名称", "LabelRSSFeedOpen": "打开 RSS 源", @@ -439,13 +439,13 @@ "LabelSeason": "季", "LabelSelectAllEpisodes": "选择所有剧集", "LabelSelectEpisodesShowing": "选择正在播放的 {0} 剧集", - "LabelSelectUsers": "Select users", + "LabelSelectUsers": "选择用户", "LabelSendEbookToDevice": "发送电子书到...", "LabelSequence": "序列", "LabelSeries": "系列", "LabelSeriesName": "系列名称", "LabelSeriesProgress": "系列进度", - "LabelServerYearReview": "Server Year in Review ({0})", + "LabelServerYearReview": "服务器年度回顾 ({0})", "LabelSetEbookAsPrimary": "设置为主", "LabelSetEbookAsSupplementary": "设置为补充", "LabelSettingsAudiobooksOnly": "只有有声读物", @@ -467,8 +467,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "只有一本书的系列将从系列页面和主页书架中隐藏.", "LabelSettingsHomePageBookshelfView": "首页使用书架视图", "LabelSettingsLibraryBookshelfView": "媒体库使用书架视图", - "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", - "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "跳过继续系列中的早期书籍", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "继续系列主页书架显示系列中未开始的第一本书, 该系列至少有一本书已完成且没有正在进行的书. 启用此设置将从最远完成的书开始系列, 而不是从第一本书开始.", "LabelSettingsParseSubtitles": "解析副标题", "LabelSettingsParseSubtitlesHelp": "从有声读物文件夹中提取副标题.<br>副标题必须用 \" - \" 分隔.<br>例: \"书名 - 这里是副标题\" 则显示副标题 \"这里是副标题\"", "LabelSettingsPreferMatchedMetadata": "首选匹配的元数据", @@ -514,10 +514,10 @@ "LabelTagsAccessibleToUser": "用户可访问的标签", "LabelTagsNotAccessibleToUser": "用户无法访问标签", "LabelTasks": "正在运行的任务", - "LabelTextEditorBulletedList": "Bulleted list", - "LabelTextEditorLink": "Link", - "LabelTextEditorNumberedList": "Numbered list", - "LabelTextEditorUnlink": "Unlink", + "LabelTextEditorBulletedList": "项目符号列表", + "LabelTextEditorLink": "链接", + "LabelTextEditorNumberedList": "编号列表", + "LabelTextEditorUnlink": "取消链接", "LabelTheme": "主题", "LabelThemeDark": "黑暗", "LabelThemeLight": "明亮", @@ -564,8 +564,8 @@ "LabelViewQueue": "查看播放列表", "LabelVolume": "音量", "LabelWeekdaysToRun": "工作日运行", - "LabelYearReviewHide": "Hide Year in Review", - "LabelYearReviewShow": "See Year in Review", + "LabelYearReviewHide": "隐藏年度回顾", + "LabelYearReviewShow": "查看年度回顾", "LabelYourAudiobookDuration": "你的有声读物持续时间", "LabelYourBookmarks": "你的书签", "LabelYourPlaylists": "你的播放列表", @@ -602,7 +602,7 @@ "MessageConfirmRemoveCollection": "你确定要移除收藏 \"{0}\"?", "MessageConfirmRemoveEpisode": "你确定要移除剧集 \"{0}\"?", "MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?", - "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", + "MessageConfirmRemoveListeningSessions": "你确定要移除 {0} 收听会话吗?", "MessageConfirmRemoveNarrator": "你确定要删除演播者 \"{0}\"?", "MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?", "MessageConfirmRenameGenre": "你确定要将所有项目流派 \"{0}\" 重命名到 \"{1}\"?", @@ -779,4 +779,4 @@ "ToastSocketFailedToConnect": "网络连接失败", "ToastUserDeleteFailed": "删除用户失败", "ToastUserDeleteSuccess": "用户已删除" -} \ No newline at end of file +} From 6c9a81147249f195709c951c9203a99d851c6663 Mon Sep 17 00:00:00 2001 From: basti <sebastian@niedner.xyz> Date: Wed, 3 Apr 2024 16:18:13 +0200 Subject: [PATCH 0504/2145] Add ui and settings for OpenID Signing Algorithm --- client/pages/config/authentication.vue | 8 ++++++++ server/Auth.js | 6 ++++-- server/objects/settings/ServerSettings.js | 9 +++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index a85433c5..260093b5 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -58,6 +58,8 @@ <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="openidTokenSigningAlgorithm" v-model="newAuthSettings.authOpenIDTokenSigningAlgorithm" :disabled="savingSettings" :label="'Signing Algorithm'" 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="sm:pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" /> @@ -187,6 +189,7 @@ export default { 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 + if (data.id_token_signing_algorithm) this.newAuthSettings.authOpenIDTokenSigningAlgorithm = data.id_token_signing_algorithm }) .catch((error) => { console.error('Failed to receive data', error) @@ -225,6 +228,11 @@ export default { isValid = false } + if (!this.newAuthSettings.authOpenIDTokenSigningAlgorithm) { + this.$toast.error('Signing Algorithm required') + isValid = false + } + function isValidRedirectURI(uri) { // Check for somestring://someother/string const pattern = new RegExp('^\\w+://[\\w\\.-]+(/[\\w\\./-]*)*$', 'i') diff --git a/server/Auth.js b/server/Auth.js index 8ba87509..f9fe6e5c 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -85,7 +85,8 @@ class Auth { token_endpoint: global.ServerSettings.authOpenIDTokenURL, userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL, jwks_uri: global.ServerSettings.authOpenIDJwksURL, - end_session_endpoint: global.ServerSettings.authOpenIDLogoutURL + end_session_endpoint: global.ServerSettings.authOpenIDLogoutURL, + id_token_signed_response_alg: global.ServerSettings.authOpenIDTokenSigningAlgorithm }).Client const openIdClient = new openIdIssuerClient({ client_id: global.ServerSettings.authOpenIDClientID, @@ -650,7 +651,8 @@ class Auth { token_endpoint: data.token_endpoint, userinfo_endpoint: data.userinfo_endpoint, end_session_endpoint: data.end_session_endpoint, - jwks_uri: data.jwks_uri + jwks_uri: data.jwks_uri, + id_token_signing_algorithm: data.id_token_signing_alg_values_supported?.[0] }) }).catch((error) => { Logger.error(`[Auth] Failed to get openid configuration at "${configUrl}"`, error) diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 5c2da381..f107d707 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -68,13 +68,14 @@ class ServerSettings { this.authOpenIDLogoutURL = null this.authOpenIDClientID = null this.authOpenIDClientSecret = null + this.authOpenIDTokenSigningAlgorithm = 'RS256' this.authOpenIDButtonText = 'Login with OpenId' this.authOpenIDAutoLaunch = false this.authOpenIDAutoRegister = false this.authOpenIDMatchExistingBy = null this.authOpenIDMobileRedirectURIs = ['audiobookshelf://oauth'] this.authOpenIDGroupClaim = '' - this.authOpenIDAdvancedPermsClaim = '' + this.authOpenIDAdvancedPermsClaim = '' if (settings) { this.construct(settings) @@ -127,6 +128,7 @@ class ServerSettings { this.authOpenIDLogoutURL = settings.authOpenIDLogoutURL || null this.authOpenIDClientID = settings.authOpenIDClientID || null this.authOpenIDClientSecret = settings.authOpenIDClientSecret || null + this.authOpenIDTokenSigningAlgorithm = settings.authOpenIDTokenSigningAlgorithm || 'RS256' this.authOpenIDButtonText = settings.authOpenIDButtonText || 'Login with OpenId' this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister @@ -217,6 +219,7 @@ class ServerSettings { authOpenIDLogoutURL: this.authOpenIDLogoutURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client + authOpenIDTokenSigningAlgorithm: this.authOpenIDTokenSigningAlgorithm, authOpenIDButtonText: this.authOpenIDButtonText, authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, authOpenIDAutoRegister: this.authOpenIDAutoRegister, @@ -252,7 +255,8 @@ class ServerSettings { this.authOpenIDUserInfoURL && this.authOpenIDJwksURL && this.authOpenIDClientID && - this.authOpenIDClientSecret + this.authOpenIDClientSecret && + this.authOpenIDTokenSigningAlgorithm } get authenticationSettings() { @@ -267,6 +271,7 @@ class ServerSettings { authOpenIDLogoutURL: this.authOpenIDLogoutURL, authOpenIDClientID: this.authOpenIDClientID, // Do not return to client authOpenIDClientSecret: this.authOpenIDClientSecret, // Do not return to client + authOpenIDTokenSigningAlgorithm: this.authOpenIDTokenSigningAlgorithm, authOpenIDButtonText: this.authOpenIDButtonText, authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, authOpenIDAutoRegister: this.authOpenIDAutoRegister, From 304d0f6d43684b43f1db169856ba75b253267d7f Mon Sep 17 00:00:00 2001 From: basti <sebastian@niedner.xyz> Date: Wed, 3 Apr 2024 22:52:49 +0200 Subject: [PATCH 0505/2145] id_token_signed_respo... should be in new Client --- server/Auth.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index f9fe6e5c..5d376fd6 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -85,12 +85,12 @@ class Auth { token_endpoint: global.ServerSettings.authOpenIDTokenURL, userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL, jwks_uri: global.ServerSettings.authOpenIDJwksURL, - end_session_endpoint: global.ServerSettings.authOpenIDLogoutURL, - id_token_signed_response_alg: global.ServerSettings.authOpenIDTokenSigningAlgorithm + end_session_endpoint: global.ServerSettings.authOpenIDLogoutURL }).Client const openIdClient = new openIdIssuerClient({ client_id: global.ServerSettings.authOpenIDClientID, - client_secret: global.ServerSettings.authOpenIDClientSecret + client_secret: global.ServerSettings.authOpenIDClientSecret, + id_token_signed_response_alg: global.ServerSettings.authOpenIDTokenSigningAlgorithm }) passport.use('openid-client', new OpenIDClient.Strategy({ client: openIdClient, From c5785e9c200c10259944c926b17923460d28917b Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 3 Apr 2024 18:41:41 -0500 Subject: [PATCH 0506/2145] Update:Increase breakpoint for player to change buttons to two lines #2799 --- client/components/app/MediaPlayerContainer.vue | 8 ++++---- client/components/player/PlayerPlaybackControls.vue | 8 ++++---- client/components/player/PlayerUi.vue | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/components/app/MediaPlayerContainer.vue b/client/components/app/MediaPlayerContainer.vue index 120231dc..081cd708 100644 --- a/client/components/app/MediaPlayerContainer.vue +++ b/client/components/app/MediaPlayerContainer.vue @@ -1,10 +1,10 @@ <template> - <div v-if="streamLibraryItem" id="mediaPlayerContainer" 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 v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 lg:h-40 z-50 bg-primary px-2 lg:px-4 pb-1 lg:pb-4 pt-2"> <div id="videoDock" /> - <div class="absolute left-2 top-2 md:left-4 cursor-pointer"> + <div class="absolute left-2 top-2 lg: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="flex items-start mb-6 lg: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"> {{ title }} @@ -29,7 +29,7 @@ </div> <div class="flex-grow" /> <ui-tooltip direction="top" :text="$strings.LabelClosePlayer"> - <button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button> + <button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button> </ui-tooltip> </div> <player-ui diff --git a/client/components/player/PlayerPlaybackControls.vue b/client/components/player/PlayerPlaybackControls.vue index f3eabab0..91b67dad 100644 --- a/client/components/player/PlayerPlaybackControls.vue +++ b/client/components/player/PlayerPlaybackControls.vue @@ -1,8 +1,8 @@ <template> - <div class="flex items-center pt-4 pb-2 md:pt-0 md:pb-2"> + <div class="flex items-center pt-4 pb-2 lg:pt-0 lg:pb-2"> <div class="flex-grow" /> <template v-if="!loading"> - <ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 md:mr-8"> + <ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8"> <button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter"> <span class="material-icons text-2xl sm:text-3xl">first_page</span> </button> @@ -12,7 +12,7 @@ <span class="material-icons text-2xl sm:text-3xl">replay_10</span> </button> </ui-tooltip> - <button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause"> + <button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause"> <span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span> </button> <ui-tooltip direction="top" :text="$strings.ButtonJumpForward"> @@ -20,7 +20,7 @@ <span class="material-icons text-2xl sm:text-3xl">forward_10</span> </button> </ui-tooltip> - <ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 md:ml-8"> + <ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 lg:ml-8"> <button :aria-label="$strings.ButtonNextChapter" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter"> <span class="material-icons text-2xl sm:text-3xl">last_page</span> </button> diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue index 8d26dd99..48621b44 100644 --- a/client/components/player/PlayerUi.vue +++ b/client/components/player/PlayerUi.vue @@ -1,7 +1,7 @@ <template> <div class="w-full -mt-6"> <div class="w-full relative mb-1"> - <div class="absolute -top-10 md:top-0 right-0 lg:right-2 flex items-center h-full"> + <div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full"> <!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> --> <ui-tooltip direction="top" :text="$strings.LabelVolume"> From f56d9f128f1388be23bfdb2db0cc616a21a10605 Mon Sep 17 00:00:00 2001 From: Lars Kiesow <lkiesow@uos.de> Date: Thu, 4 Apr 2024 21:55:52 +0200 Subject: [PATCH 0507/2145] Separator between multiple series If a book is part of multiple series, this patch adds a separator between the series on the library item details page. With no separator, it is not immediately clear that they are separate series. --- client/pages/item/_id/index.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index d8274e72..d13c9a24 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -34,7 +34,10 @@ <p v-if="bookSubtitle" class="text-gray-200 text-xl md:text-2xl">{{ bookSubtitle }}</p> - <nuxt-link v-for="_series in seriesList" :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> {{ _series.text }}</nuxt-link> + <nuxt-link v-for="(_series, index) in seriesList" :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> + {{ _series.text + }}<span v-if="index < seriesList.length - 1">, </span> + </nuxt-link> <template v-if="!isVideo"> <p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p> From 8a29c998da4480a2b44395edcca73f0dbff80fbb Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 4 Apr 2024 17:54:43 -0500 Subject: [PATCH 0508/2145] Update item page series comma separated list to not include comma in link --- client/pages/item/_id/index.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index d13c9a24..bd7957ea 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -34,10 +34,10 @@ <p v-if="bookSubtitle" class="text-gray-200 text-xl md:text-2xl">{{ bookSubtitle }}</p> - <nuxt-link v-for="(_series, index) in seriesList" :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> - {{ _series.text - }}<span v-if="index < seriesList.length - 1">, </span> - </nuxt-link> + <template v-for="(_series, index) in seriesList"> + <nuxt-link :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7">{{ _series.text }}</nuxt-link + ><span :key="index" v-if="index < seriesList.length - 1">, </span> + </template> <template v-if="!isVideo"> <p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p> From 4a127d35b9e9b1ad93da599f7ae86e70624344b6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 5 Apr 2024 16:50:15 -0500 Subject: [PATCH 0509/2145] Update:Add client name and version to sessions table and session modal --- client/components/modals/ListeningSessionModal.vue | 9 +++++++-- client/pages/config/sessions.vue | 10 +++++----- client/pages/config/users/_id/sessions.vue | 5 +++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/client/components/modals/ListeningSessionModal.vue b/client/components/modals/ListeningSessionModal.vue index daa16f70..20753234 100644 --- a/client/components/modals/ListeningSessionModal.vue +++ b/client/components/modals/ListeningSessionModal.vue @@ -88,10 +88,11 @@ <p class="mb-1">{{ _session.mediaPlayer }}</p> <p v-if="hasDeviceInfo" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelDevice }}</p> + <p v-if="clientDisplayName" class="mb-1">{{ clientDisplayName }}</p> <p v-if="deviceInfo.ipAddress" class="mb-1">{{ deviceInfo.ipAddress }}</p> <p v-if="osDisplayName" class="mb-1">{{ osDisplayName }}</p> <p v-if="deviceInfo.browserName" class="mb-1">{{ deviceInfo.browserName }}</p> - <p v-if="clientDisplayName" class="mb-1">{{ clientDisplayName }}</p> + <p v-if="deviceDisplayName" class="mb-1">{{ deviceDisplayName }}</p> <p v-if="deviceInfo.sdkVersion" class="mb-1">SDK {{ $strings.LabelVersion }}: {{ deviceInfo.sdkVersion }}</p> <p v-if="deviceInfo.deviceType" class="mb-1">{{ $strings.LabelType }}: {{ deviceInfo.deviceType }}</p> </div> @@ -141,10 +142,14 @@ export default { if (!this.deviceInfo.osName) return null return `${this.deviceInfo.osName} ${this.deviceInfo.osVersion}` }, - clientDisplayName() { + deviceDisplayName() { if (!this.deviceInfo.manufacturer || !this.deviceInfo.model) return null return `${this.deviceInfo.manufacturer} ${this.deviceInfo.model}` }, + clientDisplayName() { + if (!this.deviceInfo.clientName) return null + return `${this.deviceInfo.clientName} ${this.deviceInfo.clientVersion || ''}` + }, playMethodName() { const playMethod = this._session.playMethod if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play' diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index 0437cfd2..e9f0914b 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -64,8 +64,8 @@ <td class="hidden md:table-cell w-26 min-w-26"> <p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p> </td> - <td class="hidden sm:table-cell w-32 min-w-32"> - <p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" /> + <td class="hidden sm:table-cell max-w-32 min-w-32"> + <p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" /> </td> <td class="text-center w-24 min-w-24 sm:w-32 sm:min-w-32"> <p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p> @@ -127,8 +127,8 @@ <td class="hidden md:table-cell"> <p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p> </td> - <td class="hidden sm:table-cell"> - <p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" /> + <td class="hidden sm:table-cell max-w-32 min-w-32"> + <p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" /> </td> <td class="text-center"> <p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p> @@ -394,9 +394,9 @@ export default { getDeviceInfoString(deviceInfo) { if (!deviceInfo) return '' var lines = [] + if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`) if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`) if (deviceInfo.browserName) lines.push(deviceInfo.browserName) - if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion}`) if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`) if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`) diff --git a/client/pages/config/users/_id/sessions.vue b/client/pages/config/users/_id/sessions.vue index 4dd4e643..a8c92d0b 100644 --- a/client/pages/config/users/_id/sessions.vue +++ b/client/pages/config/users/_id/sessions.vue @@ -36,8 +36,8 @@ <td class="hidden md:table-cell"> <p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p> </td> - <td class="hidden sm:table-cell"> - <p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" /> + <td class="hidden sm:table-cell min-w-32 max-w-32"> + <p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" /> </td> <td class="text-center"> <p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p> @@ -193,6 +193,7 @@ export default { getDeviceInfoString(deviceInfo) { if (!deviceInfo) return '' var lines = [] + if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`) if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`) if (deviceInfo.browserName) lines.push(deviceInfo.browserName) From 8dc0f2c67cf83735e7c365dd71879da5e008364f Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sat, 6 Apr 2024 17:48:40 -0500 Subject: [PATCH 0510/2145] Fix:Duplicate keys error when the same library item is shown twice in continue series --- client/components/app/BookShelfRow.vue | 2 +- client/components/widgets/ItemSlider.vue | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/client/components/app/BookShelfRow.vue b/client/components/app/BookShelfRow.vue index 60632db7..5b2c4258 100644 --- a/client/components/app/BookShelfRow.vue +++ b/client/components/app/BookShelfRow.vue @@ -4,7 +4,7 @@ <div class="w-full h-full pt-6"> <div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center"> <template v-for="(entity, index) in shelf.entities"> - <cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editItem" /> + <cards-lazy-book-card :key="`${entity.id}-${index}`" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editItem" /> </template> </div> <div v-if="shelf.type === 'episode'" class="flex items-center"> diff --git a/client/components/widgets/ItemSlider.vue b/client/components/widgets/ItemSlider.vue index 7ee1f390..da6eeba4 100644 --- a/client/components/widgets/ItemSlider.vue +++ b/client/components/widgets/ItemSlider.vue @@ -13,7 +13,21 @@ <div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled"> <div class="flex" :style="{ height: height + 'px' }"> <template v-for="(item, index) in items"> - <cards-lazy-book-card :key="item.id + '-' + shelfId" :ref="`slider-item-${item.id}`" :index="index" :book-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="bookshelfView" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @edit="editItem" @select="selectItem" @hook:updated="setScrollVars" /> + <cards-lazy-book-card + :key="item.id + '-' + shelfId + '-' + index" + :ref="`slider-item-${item.id}`" + :index="index" + :book-mount="item" + :height="cardHeight" + :width="cardWidth" + :book-cover-aspect-ratio="bookCoverAspectRatio" + :bookshelf-view="bookshelfView" + :continue-listening-shelf="continueListeningShelf" + class="relative mx-2" + @edit="editItem" + @select="selectItem" + @hook:updated="setScrollVars" + /> </template> </div> </div> From 41af913280b77c0dec93d44fafd096da0f530fb6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 7 Apr 2024 16:24:23 -0500 Subject: [PATCH 0511/2145] Update:Edit item cover tab UI for small screen sizes #2832 --- client/components/modals/item/tabs/Cover.vue | 18 +++++++++--------- client/components/ui/FileInput.vue | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/components/modals/item/tabs/Cover.vue b/client/components/modals/item/tabs/Cover.vue index 056fdcb3..ed2f7ecb 100644 --- a/client/components/modals/item/tabs/Cover.vue +++ b/client/components/modals/item/tabs/Cover.vue @@ -1,7 +1,7 @@ <template> <div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative"> - <div class="flex flex-wrap mb-4"> - <div class="relative"> + <div class="flex flex-col sm:flex-row mb-4"> + <div class="relative self-center"> <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <!-- book cover overlay --> @@ -14,7 +14,7 @@ </div> </div> </div> - <div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0"> + <div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-6 md:mt-0"> <div class="flex items-center"> <div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32"> <ui-file-input ref="fileInput" @change="fileUploadSelected"> @@ -49,20 +49,20 @@ </div> </div> <form @submit.prevent="submitSearchForm"> - <div class="flex items-center justify-start -mx-1 h-20"> - <div class="w-48 px-1"> + <div class="flex flex-wrap sm:flex-nowrap items-center justify-start -mx-1"> + <div class="w-48 flex-grow p-1"> <ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small /> </div> - <div class="w-72 px-1"> + <div class="w-72 flex-grow p-1"> <ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" /> </div> - <div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 px-1"> + <div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 flex-grow p-1"> <ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" /> </div> - <ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn> + <ui-btn class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn> </div> </form> - <div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full"> + <div v-if="hasSearched" class="flex items-center flex-wrap justify-center sm:max-h-80 sm:overflow-y-scroll mt-2 max-w-full"> <p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p> <template v-for="cover in coversFound"> <div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)"> diff --git a/client/components/ui/FileInput.vue b/client/components/ui/FileInput.vue index b3064887..dcbd6a8a 100644 --- a/client/components/ui/FileInput.vue +++ b/client/components/ui/FileInput.vue @@ -1,7 +1,7 @@ <template> <div> <input ref="fileInput" type="file" :accept="accept" class="hidden" @change="inputChanged" /> - <ui-btn @click="clickUpload" color="primary" class="hidden md:block" type="text"><slot /></ui-btn> + <ui-btn @click="clickUpload" color="primary" class="hidden md:block w-full" type="text"><slot /></ui-btn> <ui-icon-btn @click="clickUpload" icon="upload" class="block md:hidden" /> </div> </template> From 04993dd63ddde89460c0c045bf5d85771da6a77e Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 8 Apr 2024 15:38:34 -0500 Subject: [PATCH 0512/2145] Update:Show language on book item page w/ link to filter #2834 --- client/components/content/LibraryItemDetails.vue | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/client/components/content/LibraryItemDetails.vue b/client/components/content/LibraryItemDetails.vue index 6764beb0..0b69b038 100644 --- a/client/components/content/LibraryItemDetails.vue +++ b/client/components/content/LibraryItemDetails.vue @@ -89,6 +89,14 @@ </template> </div> </div> + <div v-if="language && !isPodcast" class="flex py-0.5"> + <div class="w-24 min-w-24 sm:w-32 sm:min-w-32"> + <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelLanguage }}</span> + </div> + <div> + <nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link> + </div> + </div> <div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5"> <div class="w-24 min-w-24 sm:w-32 sm:min-w-32"> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span> @@ -182,6 +190,9 @@ export default { narrators() { return this.mediaMetadata.narrators || [] }, + language() { + return this.mediaMetadata.language || null + }, durationPretty() { if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration) From e60d2a98582a4d45f854880b85ad8f8aea3d7f60 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 8 Apr 2024 15:48:41 -0500 Subject: [PATCH 0513/2145] Add:Podcast library filter for languages and show language on podcast item page --- client/components/content/LibraryItemDetails.vue | 2 +- client/components/controls/LibraryFilterSelect.vue | 5 +++++ server/utils/queries/libraryFilters.js | 5 ++++- server/utils/queries/libraryItemsPodcastFilters.js | 2 ++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/client/components/content/LibraryItemDetails.vue b/client/components/content/LibraryItemDetails.vue index 0b69b038..7c6ac222 100644 --- a/client/components/content/LibraryItemDetails.vue +++ b/client/components/content/LibraryItemDetails.vue @@ -89,7 +89,7 @@ </template> </div> </div> - <div v-if="language && !isPodcast" class="flex py-0.5"> + <div v-if="language" class="flex py-0.5"> <div class="w-24 min-w-24 sm:w-32 sm:min-w-32"> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelLanguage }}</span> </div> diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue index 04dd11af..e5464293 100644 --- a/client/components/controls/LibraryFilterSelect.vue +++ b/client/components/controls/LibraryFilterSelect.vue @@ -235,6 +235,11 @@ export default { value: 'tags', sublist: true }, + { + text: this.$strings.LabelLanguage, + value: 'languages', + sublist: true + }, { text: this.$strings.ButtonIssues, value: 'issues', diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index 288ee6db..a2efb664 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -451,7 +451,7 @@ module.exports = { libraryId: libraryId } }, - attributes: ['tags', 'genres'] + attributes: ['tags', 'genres', 'language'] }) for (const podcast of podcasts) { if (podcast.tags?.length) { @@ -460,6 +460,9 @@ module.exports = { if (podcast.genres?.length) { podcast.genres.forEach((genre) => data.genres.add(genre)) } + if (podcast.language) { + data.languages.add(podcast.language) + } } } else { const books = await Database.bookModel.findAll({ diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 7665c89b..3e6e337c 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -51,6 +51,8 @@ module.exports = { [Sequelize.Op.gte]: 1 }) replacements.filterValue = value + } else if (group === 'languages') { + mediaWhere['language'] = value } return { From f75f0b8cc8e681a7491038c4c9f83e86e69f0e6b Mon Sep 17 00:00:00 2001 From: apocer <sebastian@niedner.xyz> Date: Tue, 9 Apr 2024 22:29:06 +0200 Subject: [PATCH 0514/2145] show dropdown if issuer has list of algorithms --- client/pages/config/authentication.vue | 22 ++++++++++++++++++++-- server/Auth.js | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 260093b5..5159c039 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -58,7 +58,8 @@ <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="openidTokenSigningAlgorithm" v-model="newAuthSettings.authOpenIDTokenSigningAlgorithm" :disabled="savingSettings" :label="'Signing Algorithm'" class="mb-2" /> + <ui-dropdown v-if="hasSupportedSigningAlgorithms" v-model="newAuthSettings.authOpenIDTokenSigningAlgorithm" :items="openIdSigningAlgorithmsSupportedByIssuer" :label="'Signing Algorithm'" :disabled="savingSettings" class="mb-2" /> + <ui-text-input-with-label v-else ref="openidTokenSigningAlgorithm" v-model="newAuthSettings.authOpenIDTokenSigningAlgorithm" :disabled="savingSettings" :label="'Signing Algorithm'" 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="sm:pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" /> @@ -140,6 +141,7 @@ export default { enableOpenIDAuth: false, showCustomLoginMessage: false, savingSettings: false, + openIdSigningAlgorithmsSupportedByIssuer: [], newAuthSettings: {} } }, @@ -162,6 +164,9 @@ export default { value: 'username' } ] + }, + hasSupportedSigningAlgorithms() { + return this.openIdSigningAlgorithmsSupportedByIssuer.length > 0 } }, methods: { @@ -180,6 +185,19 @@ export default { this.newAuthSettings.authOpenIDIssuerURL = this.newAuthSettings.authOpenIDIssuerURL.replace('/.well-known/openid-configuration', '') } + const setSupportedSigningAlgorithms = (algorithms) => { + this.openIdSigningAlgorithmsSupportedByIssuer = algorithms + + if(!algorithms || algorithms.length === 0) return + + // If a signing algorithm is already selected, then keep it, when it is still supported. + // But if it is not supported, then select one of the supported ones. + let currentAlgorithm = this.newAuthSettings.authOpenIDTokenSigningAlgorithm + if(!algorithms.includes(currentAlgorithm)) { + this.newAuthSettings.authOpenIDTokenSigningAlgorithm = algorithms[0] + } + } + this.$axios .$get(`/auth/openid/config?issuer=${issuerUrl}`) .then((data) => { @@ -189,7 +207,7 @@ export default { 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 - if (data.id_token_signing_algorithm) this.newAuthSettings.authOpenIDTokenSigningAlgorithm = data.id_token_signing_algorithm + if (data.id_token_signing_alg_values_supported) setSupportedSigningAlgorithms(data.id_token_signing_alg_values_supported) }) .catch((error) => { console.error('Failed to receive data', error) diff --git a/server/Auth.js b/server/Auth.js index 5d376fd6..827870b0 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -652,7 +652,7 @@ class Auth { userinfo_endpoint: data.userinfo_endpoint, end_session_endpoint: data.end_session_endpoint, jwks_uri: data.jwks_uri, - id_token_signing_algorithm: data.id_token_signing_alg_values_supported?.[0] + id_token_signing_alg_values_supported: data.id_token_signing_alg_values_supported }) }).catch((error) => { Logger.error(`[Auth] Failed to get openid configuration at "${configUrl}"`, error) From cd7ecb99330861ffa6f9be008e8d5c7c26c6ca70 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Tue, 9 Apr 2024 17:54:09 -0500 Subject: [PATCH 0515/2145] Update:User permission tags accessible to user are alphabetized #2667 --- server/controllers/MiscController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 4c4a06ff..274affa7 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -284,7 +284,7 @@ class MiscController { } res.json({ - tags: tags + tags: tags.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) }) } From 710d6af4b3ec41a73d8f0ac52717b8f5ab9094a4 Mon Sep 17 00:00:00 2001 From: soaibsafi <soaib.safi@gmail.com> Date: Wed, 10 Apr 2024 19:46:39 +0200 Subject: [PATCH 0516/2145] Adds Bengali translation --- client/plugins/i18n.js | 1 + client/strings/bn.json | 782 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 783 insertions(+) create mode 100644 client/strings/bn.json diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index ff7af170..a13675c9 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 = { + 'bn': { label: 'বাংলা', dateFnsLocale: 'bn' }, 'cs': { label: 'Čeština', dateFnsLocale: 'cs' }, 'da': { label: 'Dansk', dateFnsLocale: 'da' }, 'de': { label: 'Deutsch', dateFnsLocale: 'de' }, diff --git a/client/strings/bn.json b/client/strings/bn.json new file mode 100644 index 00000000..d80a0bb1 --- /dev/null +++ b/client/strings/bn.json @@ -0,0 +1,782 @@ +{ + "ButtonAdd": "যোগ করুন", + "ButtonAddChapters": "অধ্যায় যোগ করুন", + "ButtonAddDevice": "ডিভাইস যোগ করুন", + "ButtonAddLibrary": "লাইব্রেরি যোগ করুন", + "ButtonAddPodcasts": "পডকাস্ট যোগ করুন", + "ButtonAddUser": "ব্যবহারকারী যোগ করুন", + "ButtonAddYourFirstLibrary": "আপনার প্রথম লাইব্রেরি যোগ করুন", + "ButtonApply": "প্রয়োগ করুন", + "ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন", + "ButtonAuthors": "লেখক", + "ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন", + "ButtonCancel": "বাতিল করুন", + "ButtonCancelEncode": "এনকোড বাতিল করুন", + "ButtonChangeRootPassword": "রুট পাসওয়ার্ড পরিবর্তন করুন", + "ButtonCheckAndDownloadNewEpisodes": "নতুন পর্বগুলি পরীক্ষা এবং ডাউনলোড করুন", + "ButtonChooseAFolder": "একটি ফোল্ডার চয়ন করুন", + "ButtonChooseFiles": "ফাইল চয়ন করুন", + "ButtonClearFilter": "ফিল্টার পরিষ্কার করুন", + "ButtonCloseFeed": "ফিড বন্ধ করুন", + "Button Collections": "সংগ্রহ", + "ButtonConfigureScanner": "স্ক্যানার কনফিগার করুন", + "ButtonCreate": "তৈরি করুন", + "ButtonCreateBackup": "ব্যাকআপ তৈরি করুন", + "ButtonDelete": "মুছুন", + "ButtonDownloadQueue": "সারি", + "ButtonEdit": "সম্পাদনা করুন", + "ButtonEditChapters": "অধ্যায় সম্পাদনা করুন", + "ButtonEditPodcast": "পডকাস্ট সম্পাদনা করুন", + "ButtonForceReScan": "জোরপূর্বক পুনরায় স্ক্যান করুন", + "ButtonFullPath": "সম্পূর্ণ পথ", + "ButtonHide": "লুকান", + "ButtonHome": "নীড়", + "ButtonIssues": "ইস্যু", + "ButtonJumpBackward": "পিছনে লাফ দিন", + "ButtonJumpForward": "সামনে লাফ দিন", + "ButtonLatest": "সর্বশেষ", + "ButtonLibrary": "লাইব্রেরি", + "ButtonLogout": "লগআউট", + "ButtonLookup": "সন্ধান", + "ButtonManageTracks": "ট্র্যাকগুলি পরিচালনা করুন", + "ButtonMapChapterTitles": "অধ্যায়ের শিরোনাম ম্যাপ করুন", + "ButtonMatchAllAuthors": "সমস্ত লেখকের সাথে মিল করুন", + "ButtonMatchBooks": "বইগুলো মিল করুন", + "ButtonNevermind": "কিছু মনে করবেন না", + "ButtonNext": "পরবর্তী", + "ButtonNextChapter": "পরবর্তী অধ্যায়", + "ButtonOk": "ঠিক আছে", + "ButtonOpenFeed": "ফিড খুলুন", + "ButtonOpenManager": "ম্যানেজার খুলুন", + "ButtonPause": "বিরতি", + "ButtonPlay": "বাজান", + "ButtonPlaying": "বাজছে", + "ButtonPlaylists": "প্লেলিস্ট", + "ButtonPrevious": "পূর্ববর্তী", + "ButtonPrevious Chapter": "আগের অধ্যায়", + "ButtonPurgeAllCache": "সমস্ত ক্যাশে পরিষ্কার করুন", + "ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন", + "ButtonPurgeMediaProgress": "মিডিয়া ক্যাশে পরিষ্কার করুন", + "ButtonQueueAddItem": "সারিতে যোগ করুন", + "ButtonQueueRemoveItem": "সারি থেকে মুছে ফেলুন", + "ButtonQuickMatch": "দ্রুত ম্যাচ", + "ButtonRead": "পড়ুন", + "ButtonRefresh": "রিফ্রেশ", + "ButtonRemove": "মুছে ফেলুন", + "ButtonRemoveAll": "সব মুছে ফেলুন", + "ButtonRemoveAllLibraryItems": "সমস্ত লাইব্রেরি আইটেম মুছে ফেলুন", + "ButtonRemoveFromContinueListening": "শোনা চালিয়ে যাওয়া থেকে মুছে ফেলুন", + "ButtonRemoveFromContinueReading": "পঠন চালিয়ে যান থেকে মুছে ফেলুন", + "ButtonRemoveSeriesFromContinueSeries": "কন্টিনিউ সিরিজ থেকে সিরিজ মুছে ফেলুন", + "ButtonReScan": "পুনরায় স্ক্যান", + "ButtonReset": "রিসেট", + "ButtonResetToDefault": "ডিফল্টে পুনরায় সেট করুন", + "ButtonRestore": "পুনরুদ্ধার করুন", + "ButtonSave": "সংরক্ষণ করুন", + "ButtonSaveAndClose": "সংরক্ষণ এবং বন্ধ করুন", + "ButtonSaveTracklist": "ট্র্যাকলিস্ট সংরক্ষণ করুন", + "ButtonScan": "স্ক্যান", + "ButtonScanLibrary": "স্ক্যান লাইব্রেরি", + "ButtonSearch": "অনুসন্ধান", + "ButtonSelectFolderPath": "ফোল্ডারের পথ নির্বাচন করুন", + "ButtonSeries": "সিরিজ", + "ButtonSetChaptersFromTracks": "ট্র্যাক থেকে অধ্যায় সেট করুন", + "ButtonShare": "শেয়ার করুন", + "ButtonShiftTimes": "সময় শিফট করুন", + "ButtonShow": "দেখান", + "ButtonStartM4BEncode": "M4B এনকোড শুরু করুন", + "ButtonStartMetadataEmbed": "মেটাডেটা এম্বেড শুরু করুন", + "ButtonSubmit": "জমা দিন", + "ButtonTest": "পরীক্ষা", + "ButtonUpload": "আপলোড", + "ButtonUploadBackup": "আপলোড ব্যাকআপ", + "ButtonUploadCover": "কভার আপলোড করুন", + "ButtonUploadOPMLFile": "OPML ফাইল আপলোড করুন", + "ButtonUserDelete": "ব্যবহারকারী {0} মুছুন", + "ButtonUserEdit": "ব্যবহারকারী {0} সম্পাদনা করুন", + "ButtonViewAll": "সমস্ত দেখুন", + "ButtonYes": "হ্যাঁ", + "ErrorUploadFetchMetadataAPI": "মেটাডেটা আনতে ত্রুটি হচ্ছে", + "ErrorUploadFetchMetadataNoResults": "মেটাডেটা আনা যায়নি - শিরোনাম এবং/অথবা লেখক আপডেট করার চেষ্টা করুন", + "ErrorUploadLacksTitle": "একটি শিরোনাম থাকতে হবে", + "HeaderAccount": "অ্যাকাউন্ট", + "HeaderAdvanced": "অ্যাডভান্সড", + "HeaderAppriseNotificationSettings": "বিজ্ঞপ্তি সেটিংস অবহিত করুন", + "HeaderAudiobookTools": "অডিওবই ফাইল ম্যানেজমেন্ট টুলস", + "HeaderAudioTracks": "অডিও ট্র্যাকস", + "HeaderAuthentication": "প্রমাণীকরণ", + "HeaderBackups": "ব্যাকআপ", + "HeaderChangePassword": "পাসওয়ার্ড পরিবর্তন করুন", + "HeaderChapters": "অধ্যায়", + "HeaderChooseAFolder": "একটি ফোল্ডার চয়ন করুন", + "HeaderCollection": "সংগ্রহ", + "HeaderCollectionItems": "সংগ্রহ আইটেম", + "HeaderCover": "কভার", + "HeaderCurrentDownloads": "বর্তমান ডাউনলোডগুলি", + "HeaderCustomMetadataProviders": "কাস্টম মেটাডেটা প্রদানকারী", + "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": "বিজ্ঞপ্তি", + "HeaderOpenIDConnectAuthentication": "ওপেনআইডি সংযোগ প্রমাণীকরণ", + "HeaderOpenRSSFeed": "আরএসএস ফিড খুলুন", + "HeaderOtherFiles": "অন্যান্য ফাইল", + "HeaderPasswordAuthentication": "পাসওয়ার্ড প্রমাণীকরণ", + "HeaderPermissions": "অনুমতি", + "HeaderPlayerQueue": "প্লেয়ার সারি", + "HeaderPlaylist": "প্লেলিস্ট", + "HeaderPlaylistItems": "প্লেলিস্ট আইটেম", + "HeaderPodcastsToAdd": "যোগ করার জন্য পডকাস্ট", + "HeaderPreviewCover": "কভার ্দেখুন", + "HeaderRemoveEpisode": "পর্বটি সরান", + "HeaderRemoveEpisodes": "{0}টি পর্ব সরান", + "HeaderRSSFeedGeneral": "আরএসএস বিবরণ", + "HeaderRSSFeedIsOpen": "আরএসএস ফিড খোলা আছে", + "HeaderRSSFeeds": "আরএসএস ফিড", + "HeaderSavedMediaProgress": "মিডিয়া সংরক্ষণের অগ্রগতি", + "HeaderSchedule": "সময়সূচী", + "HeaderScheduleLibraryScans": "স্বয়ংক্রিয় লাইব্রেরি স্ক্যানের সময়সূচী", + "HeaderSession": "সেশন", + "HeaderSetBackupSchedule": "ব্যাকআপ সময়সূচী সেট করুন", + "HeaderSettings": "সেটিংস", + "HeaderSettingsDisplay": "প্রদর্শন", + "HeaderSettingsExperimental": "পরীক্ষামূলক ফিচার", + "HeaderSettingsGeneral": "সাধারণ", + "HeaderSettingsScanner": "স্ক্যানার", + "HeaderSleepTimer": "স্লিপ টাইমার", + "HeaderStatsLargestItems": "সবচেয়ে বড় আইটেম", + "HeaderStatsLongestItems": "দীর্ঘতম আইটেম (ঘন্টা)", + "HeaderStatsMinutesListeningChart": "মিনিট শ্রবণ (গত ৭ দিন)", + "HeaderStatsRecentSessions": "সাম্প্রতিক সেশন", + "HeaderStatsTop10Authors": "শীর্ষ ১০ জন লেখক", + "HeaderStatsTop5Genres": "শীর্ষ ৫ টি ঘরানা", + "HeaderTableOfContents": "বিষয়বস্তুর সারণী", + "HeaderTools": "টুলস", + "HeaderUpdateAccount": "অ্যাকাউন্ট আপডেট করুন", + "HeaderUpdateAuthor": "লেখক আপডেট করুন", + "HeaderUpdateDetails": "বিশদ আপডেট করুন", + "HeaderUpdateLibrary": "লাইব্রেরি আপডেট করুন", + "HeaderUsers": "ব্যবহারকারীরা", + "HeaderYearReview": "বাৎসরিক পর্যালোচনা {0}", + "HeaderYourStats": "আপনার পরিসংখ্যান", + "LabelAbridged": "সংক্ষিপ্ত", + "LabelAccountType": "অ্যাকাউন্টের প্রকার", + "LabelAccountTypeAdmin": "প্রশাসন", + "LabelAccountTypeGuest": "অতিথি", + "LabelAccountTypeUser": "ব্যবহারকারী", + "LabelActivity": "ক্রিয়াকলাপ", + "Label Added": "যোগ করা হয়েছে", + "LabelAddedAt": "এতে যোগ করা হয়েছে", + "LabelAddToCollection": "সংগ্রহে যোগ করুন", + "LabelAddToCollectionBatch": "সংগ্রহে {0}টি বই যোগ করুন", + "LabelAddToPlaylist": "প্লেলিস্টে যোগ করুন", + "LabelAddToPlaylistBatch": "প্লেলিস্টে {0}টি আইটেম যোগ করুন", + "LabelAdminUsersOnly": "শুধু অ্যাডমিন ব্যবহারকারী", + "LabelAll": "সব", + "LabelAllUsers": "সমস্ত ব্যবহারকারী", + "LabelAllUsersExcludingGuests": "অতিথি ব্যতীত সকল ব্যবহারকারী", + "LabelAllUsersIncludingGuests": "অতিথি সহ সকল ব্যবহারকারী", + "LabelAlreadyInYourLibrary": "ইতিমধ্যেই আপনার লাইব্রেরিতে রয়েছে", + "LabelAppend": "সংযোজন", + "LabelAuthor": "লেখক", + "LabelAuthorFirstLast": "লেখক (প্রথম শেষ)", + "LabelAuthorLastFirst": "লেখক (শেষ, প্রথম)", + "LabelAuthors": "লেখকগণ", + "LabelAutoDownloadEpisodes": "স্বয়ংক্রিয় ডাউনলোড পর্ব", + "LabelAutoFetchMetadata": "স্বয়ংক্রিয় ফেচ মেটাডেটা", + "LabelAutoFetchMetadataHelp": "আপলোডিং স্ট্রিমলাইন করার জন্য শিরোনাম, লেখক এবং সিরিজের জন্য মেটাডেটা খুঁজুন। আপলোড করার পরে অতিরিক্ত মেটাডেটা মিলতে হতে পারে।", + "LabelAutoLaunch": "স্বয়ংক্রিয় আরম্ভ", + "LabelAutoLaunchDescription": "লগইন পৃষ্ঠায় নেভিগেট করার সময় স্বয়ংক্রিয়ভাবে অনুমোদন প্রদানকারীর কাছে পুনঃনির্দেশ করুন (হস্তকৃত ওভাররাইড পথ <code>/login?autoLaunch=0</code>)", + "LabelAutoRegister": "স্বয়ংক্রিয় নিবন্ধন", + "LabelAutoRegisterDescription": "লগ ইন করার পর স্বয়ংক্রিয়ভাবে নতুন ব্যবহারকারী তৈরি করুন", + "LabelBackToUser": "ব্যবহারকারীর কাছে ফিরে যান", + "LabelBackupLocation": "ব্যাকআপ অবস্থান", + "LabelBackupsEnableAutomaticBackups": "স্বয়ংক্রিয় ব্যাকআপ সক্ষম করুন", + "LabelBackupsEnableAutomaticBackupsHelp": "ব্যাকআপগুলি /মেটাডাটা/ব্যাকআপে সংরক্ষিত", + "LabelBackupsMaxBackupSize": "সর্বোচ্চ ব্যাকআপ আকার (GB-তে)", + "LabelBackupsMaxBackupSizeHelp": "ভুল কনফিগারেশনের বিরুদ্ধে সুরক্ষা হিসেবে ব্যাকআপগুলি ব্যর্থ হবে যদি তারা কনফিগার করা আকার অতিক্রম করে।", + "LabelBackupsNumberToKeep": "ব্যাকআপের সংখ্যা রাখুন", + "LabelBackupsNumberToKeepHelp": "এক সময়ে শুধুমাত্র ১ টি ব্যাকআপ সরানো হবে তাই যদি আপনার কাছে ইতিমধ্যে এর চেয়ে বেশি ব্যাকআপ থাকে তাহলে আপনাকে ম্যানুয়ালি সেগুলি সরিয়ে ফেলতে হবে।", + "LabelBitrate": "বিটরেট", + "LabelBooks": "বইগুলো", + "LabelButtonText": "ঘর পাঠ্য", + "LabelChangePassword": "পাসওয়ার্ড পরিবর্তন করুন", + "LabelChannels": "চ্যানেল", + "LabelChapters": "অধ্যায়", + "LabelChaptersFound": "অধ্যায় পাওয়া গেছে", + "LabelChapterTitle": "অধ্যায়ের শিরোনাম", + "LabelClickForMoreInfo": "আরো তথ্যের জন্য ক্লিক করুন", + "LabelClosePlayer": "প্লেয়ার বন্ধ করুন", + "LabelCodec": "কোডেক", + "LabelCollapseSeries": "সিরিজ সঙ্কুচিত করুন", + "LabelCollection": "সংগ্রহ", + "Label Collections": "সংগ্রহ", + "LabelComplete": "সম্পূর্ণ", + "LabelConfirmPassword": "পাসওয়ার্ড নিশ্চিত করুন", + "LabelContinueListening": "শোনা চালিয়ে যান", + "LabelContinueReading": "পড়া চালিয়ে যান", + "LabelContinueSeries": "সিরিজ চালিয়ে যান", + "LabelCover": "কভার", + "LabelCoverImageURL": "ছবির কভারের URL", + "LabelCreatedAt": "তৈরি করা হয়েছে", + "LabelCronExpression": "Cron এক্সপ্রেশন", + "LabelCurrent": "বর্তমান", + "LabelCurrently": "বর্তমানে:", + "LabelCustomCronExpression": "কাস্টম Cron এক্সপ্রেশন:", + "LabelDatetime": "তারিখ সময়", + "LabelDeleteFromFileSystemCheckbox": "ফাইল সিস্টেম থেকে মুছে ফেলুন (শুধু ডাটাবেস থেকে সরাতে টিক চিহ্ন মুক্ত করুন)", + "LabelDescription": "বিবরণ", + "LabelDeselectAll": "সমস্ত অনির্বাচিত করুন", + "LabelDevice": "ডিভাইস", + "LabelDeviceInfo": "ডিভাইস তথ্য", + "LabelDeviceIsAvailableTo": "ডিভাইস এর জন্য উপলব্ধ...", + "LabelDirectory": "ডিরেক্টরি", + "LabelDiscFromFilename": "ফাইলের নাম থেকে ডিস্ক", + "LabelDiscFromMetadata": "মেটাডেটা থেকে ডিস্ক", + "LabelDiscover": "আবিষ্কার", + "LabelDownload": "ডাউনলোড করুন", + "LabelDownloadNEpisodes": "{0}টি পর্ব ডাউনলোড করুন", + "LabelDuration": "সময়কাল", + "LabelDurationFound": "সময়কাল পাওয়া গেছে:", + "LabelEbook": "ই-বই", + "LabelEbooks": "ই-বইগুলো", + "LabelEdit": "সম্পাদনা করুন", + "LabelEmail": "ইমেইল", + "LabelEmailSettingsFromAddress": "ঠিকানা থেকে", + "LabelEmailSettingsSecure": "নিরাপদ", + "LabelEmailSettingsSecureHelp": "যদি সত্য হয় সার্ভারের সাথে সংযোগ করার সময় সংযোগটি TLS ব্যবহার করবে। মিথ্যা হলে TLS ব্যবহার করা হবে যদি সার্ভার STARTTLS এক্সটেনশন সমর্থন করে। বেশিরভাগ ক্ষেত্রে এই মানটিকে সত্য হিসাবে সেট করুন যদি আপনি পোর্ট 465-এর সাথে সংযোগ করছেন। পোর্ট 587 বা পোর্টের জন্য 25 এটি মিথ্যা রাখুন। (nodemailer.com/smtp/#authentication থেকে)", + "LabelEmailSettingsTestAddress": "পরীক্ষার ঠিকানা", + "LabelEmbeddedCover": "এম্বেডেড কভার", + "LabelEnable": "সক্ষম করুন", + "LabelEnd": "সমাপ্ত", + "LabelEpisode": "পর্ব", + "LabelEpisodeTitle": "পর্বের শিরোনাম", + "LabelEpisodeType": "পর্বের ধরন", + "LabelExample": "উদাহরণ", + "LabelExplicit": "বিশদ", + "LabelFeedURL": "ফিড ইউআরএল", + "LabelFetchingMetadata": "মেটাডেটা আনা হচ্ছে", + "LabelFile": "ফাইল", + "LabelFileBirthtime": "ফাইল জন্মের সময়", + "LabelFileModified": "ফাইল পরিবর্তিত", + "LabelFilename": "ফাইলের নাম", + "LabelFilterByUser": "ব্যবহারকারী দ্বারা ফিল্টারকৃত", + "LabelFindEpisodes": "পর্বগুলো খুঁজুন", + "LabelFinished": "সমাপ্ত", + "LabelFolder": "ফোল্ডার", + "LabelFolders": "ফোল্ডারগুলো", + "LabelFontBold": "বোল্ড", + "LabelFontFamily": "ফন্ট পরিবার", + "LabelFontItalic": "ইটালিক", + "LabelFontScale": "ফন্ট স্কেল", + "LabelFontStrikethrough": "অবচ্ছেদন রেখা", + "LabelFormat": "ফরম্যাট", + "LabelGenre": "ঘরানা", + "LabelGenres": "ঘরানাগুলো", + "LabelHardDeleteFile": "জোরপূর্বক ফাইল মুছে ফেলুন", + "LabelHasEbook": "ই-বই আছে", + "LabelHasSupplementaryEbook": "পরিপূরক ই-বই আছে", + "LabelHighestPriority": "সর্বোচ্চ অগ্রাধিকার", + "LabelHost": "নিমন্ত্রণকর্তা", + "LabelHour": "ঘন্টা", + "LabelIcon": "আইকন", + "LabelImageURLFromTheWeb": "ওয়েব থেকে ছবির ইউআরএল", + "LabelIncludeInTracklist": "ট্র্যাকলিস্টে অন্তর্ভুক্ত করুন", + "LabelIncomplete": "অসম্পূর্ণ", + "LabelInProgress": "প্রগতিতে আছে", + "LabelInterval": "বিরতি", + "LabelIntervalCustomDailyWeekly": "কাস্টম দৈনিক/সাপ্তাহিক", + "LabelIntervalEvery12Hours": "প্রতি ১২ ঘন্টায়", + "LabelIntervalEvery15Minutes": "প্রতি ১৫ মিনিটে", + "LabelIntervalEvery2Hours": "প্রতি ২ ঘন্টায়", + "LabelIntervalEvery30Minutes": "প্রতি ৩০ মিনিটে", + "LabelIntervalEvery6Hours": "প্রতি ৬ ঘন্টায়", + "LabelIntervalEveryDay": "প্রতিদিন", + "LabelIntervalEveryHour": "প্রতি ঘন্টা", + "LabelInvert": "উল্টানো", + "LabelItem": "আইটেম", + "LabelLanguage": "ভাষা", + "LabelLanguageDefaultServer": "সার্ভারের ডিফল্ট ভাষা", + "LabelLastBookAdded": "শেষ বই যোগ করা হয়েছে", + "LabelLastBookUpdated": "শেষ বই আপডেট করা হয়েছে", + "LabelLastSeen": "শেষ দেখা", + "LabelLastTime": "শেষ বার", + "LabelLastUpdate": "শেষ আপডেট", + "LabelLayout": "লেআউট", + "LabelLayoutSinglePage": "একক পৃষ্ঠা", + "LabelLayoutSplitPage": "বিভক্ত পৃষ্ঠা", + "LabelLess": "কম", + "LabelLibrariesAccessibleToUser": "ব্যবহারকারীর কাছে অ্যাক্সেসযোগ্য লাইব্রেরি", + "LabelLibrary": "লাইব্রেরি", + "LabelLibraryItem": "লাইব্রেরি আইটেম", + "LabelLibraryName": "লাইব্রেরির নাম", + "LabelLimit": "সীমা", + "LabelLineSpacing": "লাইন স্পেসিং", + "LabelListenAgain": "আবার শুনুন", + "LabelLogLevelDebug": "ডিবাগ", + "LabelLogLevelInfo": "তথ্য", + "LabelLogLevelWarn": "সতর্ক", + "LabelLookForNewEpisodesAfterDate": "এই তারিখের পরে নতুন পর্বগুলি সন্ধান করুন", + "LabelLowestPriority": "সর্বনিম্ন অগ্রাধিকার", + "LabelMatchExistingUsersBy": "বিদ্যমান ব্যবহারকারীদের দ্বারা মিলিত করুন", + "LabelMatchExistingUsersByDescription": "বিদ্যমান ব্যবহারকারীদের সংযোগ করার জন্য ব্যবহৃত হয়। একবার সংযুক্ত হলে, ব্যবহারকারীদের আপনার SSO প্রদানকারীর থেকে একটি অনন্য আইডি দ্বারা মিলিত হবে", + "LabelMediaPlayer": "মিডিয়া প্লেয়ার", + "LabelMediaType": "মিডিয়ার ধরন", + "LabelMetadataOrderOfPrecedenceDescription": "উচ্চ অগ্রাধিকারের মেটাডেটার উৎসগুলো নিম্ন অগ্রাধিকারের মেটাডেটা উৎসগুলোকে ওভাররাইড করবে", + "LabelMetadataProvider": "মেটাডেটা প্রদানকারী", + "LabelMetaTag": "মেটা ট্যাগ", + "LabelMetaTags": "মেটা ট্যাগগুলো", + "LabelMinute": "মিনিট", + "LabelMissing": "নিখোঁজ", + "LabelMissingEbook": "কোনও ই-বই নেই", + "LabelMissingSupplementaryEbook": "কোনও সম্পূরক ই-বই নেই", + "LabelMobileRedirectURIs": "অনুমোদিত মোবাইল রিডাইরেক্ট URIs", + "LabelMobileRedirectURIsDescription": "এটি মোবাইল অ্যাপের জন্য বৈধ পুনঃনির্দেশিত URI-এর একটি সাদা তালিকা। ডিফল্টটি হল <code>audiobookshelf://oauth</code>, যা আপনি তৃতীয় পক্ষের অ্যাপ ইন্টিগ্রেশনের জন্য অতিরিক্ত URI-এর সাথে সরাতে বা সম্পূরক করতে পারেন। একটি তারকাচিহ্ন (<code>*</code>) ব্যবহার করে একমাত্র এন্ট্রি যেকোন ইউআরআইকে অনুমতি দেয়।", + "LabelMore": "আরো", + "LabelMoreInfo": "আরো তথ্য", + "LabelName": "নাম", + "LabelNarrator": "কথক", + "LabelNarrators": "কথক", + "LabelNew": "নতুন", + "LabelNewestAuthors": "নতুন লেখক", + "LabelNewestEpisodes": "নতুনতম পর্ব", + "LabelNewPassword": "নতুন পাসওয়ার্ড", + "LabelNextBackupDate": "পরবর্তী ব্যাকআপ তারিখ", + "LabelNextScheduledRun": "পরবর্তী নির্ধারিত দৌড়", + "LabelNoEpisodesSelected": "কোন পর্ব নির্বাচন করা হয়নি", + "LabelNotes": "নোটস", + "LabelNotFinished": "সমাপ্ত হয়নি", + "LabelNotificationAppriseURL": "অবহিত URL(গুলি)", + "LabelNotificationAvailableVariables": "ব্যবহারযোগ্য ভেরিয়েবল", + "LabelNotificationBodyTemplate": "বডি টেমপ্লেট", + "LabelNotificationEvent": "ইভেন্ট বিজ্ঞপ্তি", + "LabelNotificationsMaxFailedAttempts": "সর্বোচ্চ ব্যর্থ প্রচেষ্টা", + "LabelNotificationsMaxFailedAttemptsHelp": "এটি বারবার পাঠাতে ব্যর্থ হলে বিজ্ঞপ্তি অক্ষম করা হবে", + "LabelNotificationsMaxQueueSize": "বিজ্ঞপ্তি ইভেন্টের জন্য সর্বোচ্চ সারির আকার", + "LabelNotificationsMaxQueueSizeHelp": "ইভেন্টগুলি প্রতি সেকেন্ডে ১ বার ইন্ধন করার মধ্যে সীমাবদ্ধ। সারি সর্বাধিক আকারে থাকলে ইভেন্টগুলি উপেক্ষা করা হবে। এটি বিজ্ঞপ্তি স্প্যামিং প্রতিরোধ করে।", + "LabelNotificationTitleTemplate": "শিরোনাম টেমপ্লেট", + "LabelNotStarted": "শুরু হয়নি", + "LabelNumberOfBooks": "বইয়ের সংখ্যা", + "LabelNumberOfEpisodes": "# টি পর্ব", + "LabelOpenIDAdvancedPermsClaimDescription": "ওপেনআইডি দাবির নাম যাতে অ্যাপ্লিকেশনের মধ্যে ব্যবহারকারীর ক্রিয়াকলাপের জন্য উন্নত অনুমতি রয়েছে যা অ-প্রশাসক ভূমিকাগুলিতে প্রযোজ্য হবে (<b>যদি কনফিগার করা হয়</b>)। প্রতিক্রিয়া থেকে দাবিটি অনুপস্থিত থাকলে, অ্যাক্সেস করুন ABS-তে অস্বীকার করা হবে। যদি একটি একক বিকল্প অনুপস্থিত থাকে, তাহলে এটিকে <code>false</code> হিসাবে গণ্য করা হবে। নিশ্চিত করুন যে পরিচয় প্রদানকারীর দাবি প্রত্যাশিত কাঠামোর সাথে মেলে:", + "LabelOpenIDClaims": "অ্যাডভান্সড গ্রুপ এবং পারমিশন অ্যাসাইনমেন্ট নিষ্ক্রিয় করতে নিম্নলিখিত বিকল্পগুলিকে খালি ছেড়ে দিন, তারপর স্বয়ংক্রিয়ভাবে 'ব্যবহারকারী' গ্রুপকে বরাদ্দ করা হবে।", + "LabelOpenIDGroupClaimDescription": "ওপেনআইডি দাবির নাম যাতে ব্যবহারকারীর গোষ্ঠীর একটি তালিকা থাকে। সাধারণত <code>গ্রুপ</code> হিসাবে উল্লেখ করা হয়। <b>কনফিগার করা থাকলে</b>, অ্যাপ্লিকেশনটি স্বয়ংক্রিয়ভাবে এর উপর ভিত্তি করে ব্যবহারকারীর গোষ্ঠীর সদস্যপদ নির্ধারণ করবে, শর্ত এই যে এই গোষ্ঠীগুলি কেস-অসংবেদনশীলভাবে দাবিতে 'অ্যাডমিন', 'ব্যবহারকারী' বা 'অতিথি' নাম দেওয়া হয়৷ দাবিতে একটি তালিকা থাকা উচিত এবং যদি একজন ব্যবহারকারী একাধিক গোষ্ঠীর অন্তর্গত হয় তবে অ্যাপ্লিকেশনটি বরাদ্দ করবে সর্বোচ্চ স্তরের অ্যাক্সেসের সাথে সঙ্গতিপূর্ণ ভূমিকা৷ যদি কোনও গোষ্ঠীর সাথে মেলে না, তবে অ্যাক্সেস অস্বীকার করা হবে।", + "LabelOpenRSSFeed": "আরএসএস ফিড খুলুন", + "LabelOverwrite": "পুনঃলিখিত", + "LabelPassword": "পাসওয়ার্ড", + "LabelPath": "পথ", + "LabelPermissionsAccessAllLibraries": "সমস্ত লাইব্রেরি অ্যাক্সেস করতে পারবে", + "LabelPermissionsAccessAllTags": "সমস্ত ট্যাগ অ্যাক্সেস করতে পারবে", + "LabelPermissionsAccessExplicitContent": "স্পষ্ট বিষয়বস্তু অ্যাক্সেস করতে পারে", + "LabelPermissionsDelete": "মুছে দিতে পারবে", + "LabelPermissionsDownload": "ডাউনলোড করতে পারবে", + "LabelPermissionsUpdate": "আপডেট করতে পারবে", + "LabelPermissionsUpload": "আপলোড করতে পারবে", + "LabelPersonalYearReview": "আপনার বছরের পর্যালোচনা ({0})", + "LabelPhotoPathURL": "ছবি পথ/ইউআরএল", + "LabelPlaylists": "প্লেলিস্ট", + "LabelPlayMethod": "প্লে পদ্ধতি", + "LabelPodcast": "পডকাস্ট", + "LabelPodcasts": "পডকাস্টগুলো", + "LabelPodcastSearchRegion": "পডকাস্ট অনুসন্ধান অঞ্চল", + "LabelPodcastType": "পডকাস্টের ধরন", + "LabelPort": "পোর্ট", + "LabelPrefixesToIgnore": "উপেক্ষা করার উপসর্গ (কেস সংবেদনশীল)", + "LabelPreventIndexing": "আইটিউনস এবং গুগল পডকাস্ট ডিরেক্টরি দ্বারা আপনার ফিডকে ইন্ডেক্স করা থেকে বিরত রাখুন", + "LabelPrimaryEbook": "প্রাথমিক ই-বই", + "LabelProgress": "প্রগতি", + "LabelProvider": "প্রদানকারী", + "LabelPubDate": "প্রকাশের তারিখ", + "LabelPublisher": "প্রকাশক", + "LabelPublishYear": "প্রকাশের বছর", + "LabelRead": "পড়ুন", + "LabelReadAgain": "আবার পড়ুন", + "LabelReadEbookWithoutProgress": "প্রগতি না রেখে ই-বই পড়ুন", + "LabelRecentlyAdded": "সম্প্রতি যোগ করা হয়েছে", + "LabelRecentSeries": "সাম্প্রতিক সিরিজ", + "LabelRecommended": "সুপারিশকৃত", + "LabelRedo": "পুনরায় করুন", + "LabelRegion": "অঞ্চল", + "LabelReleaseDate": "উন্মোচনের তারিখ", + "LabelRemoveCover": "কভার সরান", + "LabelRowsPerPage": "প্রতি পৃষ্ঠায় সারি", + "LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল", + "LabelRSSFeedCustomOwnerName": "কাস্টম মালিকের নাম", + "LabelRSSFeedOpen": "আরএসএস ফিড খুলুন", + "LabelRSSFeedPreventIndexing": "সূচীকরণ প্রতিরোধ করুন", + "LabelRSSFeedSlug": "আরএসএস ফিড স্লাগ", + "LabelRSSFeedURL": "আরএসএস ফিড ইউআরএল", + "LabelSearchTerm": "অনুসন্ধান শব্দ", + "LabelSearchTitle": "অনুসন্ধান শিরোনাম", + "LabelSearchTitleOrASIN": "অনুসন্ধান শিরোনাম বা ASIN", + "LabelSeason": "সেশন", + "LabelSelectAllEpisodes": "সমস্ত পর্ব নির্বাচন করুন", + "LabelSelectEpisodesShowing": "দেখানো {0}টি পর্ব নির্বাচন করুন", + "LabelSelectUsers": "ব্যবহারকারী নির্বাচন করুন", + "LabelSendEbookToDevice": "ই-বই পাঠান...", + "LabelSequence": "ক্রম", + "LabelSeries": "সিরিজ", + "LabelSeriesName": "সিরিজের নাম", + "LabelSeriesProgress": "সিরিজের অগ্রগতি", + "LabelServerYearReview": "সার্ভারের বাৎসরিক পর্যালোচনা ({0})", + "LabelSetEbookAsPrimary": "প্রাথমিক হিসাবে সেট করুন", + "LabelSetEbookAsSupplementary": "পরিপূরক হিসেবে সেট করুন", + "LabelSettingsAudiobooksOnly": "শুধুমাত্র অডিও বই", + "LabelSettingsAudiobooksOnlyHelp": "এই সেটিংটি সক্ষম করা ই-বই ফাইলগুলিকে উপেক্ষা করবে যদি না সেগুলি একটি অডিওবই ফোল্ডারের মধ্যে থাকে যে ক্ষেত্রে সেগুলিকে সম্পূরক ই-বই হিসাবে সেট করা হবে", + "LabelSettingsBookshelfViewHelp": "কাঠের তাক সহ স্কুমরফিক ডিজাইন", + "LabelSettingsChromecastSupport": "ক্রোমকাস্ট সমর্থন", + "LabelSettingsDateFormat": "তারিখ বিন্যাস", + "LabelSettingsDisableWatcher": "প্রহরী নিষ্ক্রিয় করুন", + "LabelSettingsDisableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী নিষ্ক্রিয় করুন", + "LabelSettingsDisableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে স্বয়ংক্রিয়ভাবে আইটেম যোগ/আপডেট করা অক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে", + "LabelSettingsEnableWatcher": "প্রহরী সক্ষম করুন", + "LabelSettingsEnableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী সক্ষম করুন", + "LabelSettingsEnableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে আইটেমগুলির স্বয়ংক্রিয় যোগ/আপডেট সক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে", + "LabelSettingsExperimentalFeatures": "পরীক্ষামূলক বৈশিষ্ট্য", + "LabelSettingsExperimentalFeaturesHelp": "ফিচারের বৈশিষ্ট্য যা আপনার প্রতিক্রিয়া ব্যবহার করতে পারে এবং পরীক্ষায় সহায়তা করতে পারে। গিটহাব আলোচনা খুলতে ক্লিক করুন।", + "LabelSettingsFindCovers": "কভার খুঁজুন", + "LabelSettingsFindCoversHelp": "যদি আপনার অডিওবইয়ের ফোল্ডারের ভিতরে একটি এমবেডেড কভার বা কভার ইমেজ না থাকে, তাহলে স্ক্যানার একটি কভার খোঁজার চেষ্টা করবে৷<br>দ্রষ্টব্য: এটি স্ক্যানের সময় বাড়িয়ে দেবে", + "LabelSettingsHideSingleBookSeries": "একক বই সিরিজ লুকান", + "LabelSettingsHideSingleBookSeriesHelp": "যে সিরিজগুলোতে একটি বই আছে সেগুলো সিরিজের পাতা এবং নীড় পেজের তাক থেকে লুকিয়ে রাখা হবে।", + "LabelSettingsHomePageBookshelfView": "নীড় পেজে বুকশেলফ ভিউ ব্যবহার করুন", + "LabelSettingsLibraryBookshelfView": "লাইব্রেরি বুকশেলফ ভিউ ব্যবহার করুন", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "কন্টিনিউ সিরিজে আগের বইগুলো এড়িয়ে যান", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "কন্টিনিউ সিরিজের হোম পেজ শেল্ফ প্রথম বইটি দেখায় যেটি সিরিজে শুরু হয়নি যেটিতে অন্তত একটি বই শেষ হয়েছে এবং কোনো বই চলছে না। এই সেটিংটি সক্ষম করা হলে তা শুরু না হওয়া প্রথম বইটির পরিবর্তে সবচেয়ে দূরের সম্পূর্ণ বই থেকে সিরিজ চালিয়ে যাবে। ", + "LabelSettingsParseSubtitles": "সাবটাইটেল পার্স করুন", + "LabelSettingsParseSubtitlesHelp": "অডিওবুক ফোল্ডারের নাম থেকে সাবটাইটেল বের করুন৷<br>সাবটাইটেল অবশ্যই \" - \"<br>অর্থাৎ \"বুকের শিরোনাম - এখানে একটি সাবটাইটেল\" এর সাবটাইটেল আছে \"এখানে একটি সাবটাইটেল\"", + "LabelSettingsPreferMatchedMetadata": "মিলিত মেটাডেটা পছন্দ করুন", + "LabelSettingsPreferMatchedMetadataHelp": "দ্রুত ম্যাচ ব্যবহার করার সময় মিলে যাওয়া ডেটা আইটেমের বিবরণকে ওভাররাইড করবে। ডিফল্টরূপে দ্রুত ম্যাচ শুধুমাত্র অনুপস্থিত বিশদগুলি পূরণ করবে।", + "LabelSettingsSkipMatchingBooksWithASIN": "এমন বইগুলি এড়িয়ে যান যেগুলির মধ্যে ইতিমধ্যে একটি ASIN আছে", + "LabelSettingsSkipMatchingBooksWithISBN": "ইতিমধ্যে একটি ISBN আছে এমন মেলা বইগুলি এড়িয়ে যান", + "LabelSettingsSortingIgnorePrefixes": "বাছাই করার সময় উপসর্গ উপেক্ষা করুন", + "LabelSettingsSortingIgnorePrefixesHelp": "অর্থাৎ \"বইয়ের শিরোনাম\" বইয়ের শিরোনাম \"বইয়ের শিরোনাম, \" হিসাবে সাজানো হবে উপসর্গের জন্য", + "LabelSettingsSquareBookCovers": "বর্গাকার বইয়ের কভার ব্যবহার করুন", + "LabelSettingsSquareBookCoversHelp": "প্রমাণ ১.৬:১ বইয়ের কভারের চেয়ে বর্গাকার কভার ব্যবহার করতে পছন্দ করুন", + "LabelSettingsStoreCoversWithItem": "আইটেম সহ কভার সংরক্ষণ", + "LabelSettingsStoreCoversWithItemHelp": "ডিফল্টভাবে কভারগুলি /মেটাডাটা/আইটেমগুলিতে সংরক্ষণ করা হয়, এই সেটিংটি সক্ষম করলে আপনার লাইব্রেরি আইটেম ফোল্ডারে কভারগুলি সংরক্ষণ করা হবে৷ \"কভার\" নামে শুধুমাত্র একটি ফাইল রাখা হবে", + "LabelSettingsStoreMetadataWithItem": "আইটেমের সাথে মেটাডেটা সংরক্ষণ করুন", + "LabelSettingsStoreMetadataWithItemHelp": "ডিফল্টরূপে মেটাডেটা ফাইলগুলি /মেটাডাটা/আইটেমগুলি -এ সংরক্ষণ করা হয়, এই সেটিংটি সক্ষম করলে মেটাডেটা ফাইলগুলি আপনার লাইব্রেরি আইটেম ফোল্ডারে সংরক্ষণ করা হবে", + "LabelSettingsTimeFormat": "সময় বিন্যাস", + "LabelShowAll": "সব দেখান", + "LabelSize": "আকার", + "LabelSleepTimer": "স্লিপ টাইমার", + "LabelSlug": "স্লাগ", + "LabelStart": "শুরু", + "LabelStarted": "শুরু হয়েছে", + "LabelStartedAt": "এতে শুরু হয়েছে", + "LabelStartTime": "শুরু করার সময়", + "LabelStatsAudioTracks": "অডিও ট্র্যাক", + "LabelStatsAuthors": "লেখক", + "LabelStatsBestDay": "সেরা দিন", + "LabelStatsDailyAverage": "দৈনিক গড়", + "LabelStatsDays": "দিন", + "LabelStatsDaysListened": "যেদিন শোনা হয়েছে", + "LabelStatsHours": "ঘন্টা", + "LabelStatsInARow": "এক সারিতে", + "LabelStatsItemsFinished": "আইটেম সমাপ্ত", + "LabelStatsItemsInLibrary": "লাইব্রেরির আইটেম", + "LabelStatsMinutes": "মিনিট", + "LabelStatsMinutesListening": "মিনিট শুনছেন", + "LabelStatsOverallDays": "সামগ্রিক দিন", + "LabelStatsOverallHours": "সামগ্রিক ঘন্টা", + "LabelStatsWeekListening": "সপ্তাহ শোনা", + "LabelSubtitle": "সাবটাইটেল", + "LabelSupportedFileTypes": "সমর্থিত ফাইল প্রকার", + "LabelTag": "ট্যাগ", + "LabelTags": "ট্যাগগুলো", + "LabelTagsAccessibleToUser": "ব্যবহারকারীর কাছে অ্যাক্সেসযোগ্য ট্যাগ", + "LabelTagsNotAccessibleToUser": "ট্যাগগুলি ব্যবহারকারীর কাছে অ্যাক্সেসযোগ্য নয়", + "LabelTasks": "কাজ চলছে", + "LabelTextEditorBulletedList": "বুলেটেড তালিকা", + "LabelTextEditorLink": "লিঙ্ক", + "LabelTextEditorNumberedList": "সংখ্যাযুক্ত তালিকা", + "LabelTextEditorUnlink": "বিচ্ছিন্ন", + "LabelTheme": "থিম", + "LabelThemeDark": "অন্ধকার", + "LabelThemeLight": "আলো", + "LabelTimeBase": "সময় বেস", + "LabelTimeListened": "সময় শোনা হয়েছে", + "LabelTimeListenedToday": "আজ শোনার সময়", + "LabelTimeRemaining": "{0}টি অবশিষ্ট", + "LabelTimeToShift": "সেকেন্ডে স্থানান্তরের সময়", + "LabelTitle": "শিরোনাম", + "LabelToolsEmbedMetadata": "মেটাডেটা এম্বেড করুন", + "LabelToolsEmbedMetadataDescription": "কভার ইমেজ এবং অধ্যায় সহ অডিও ফাইলগুলিতে মেটাডেটা এম্বেড করুন।", + "LabelToolsMakeM4b": "M4B অডিওবুক ফাইল তৈরি করুন", + "LabelToolsMakeM4bDescription": "এমবেডেড মেটাডেটা, কভার ইমেজ এবং অধ্যায় সহ একটি .M4B অডিওবুক ফাইল তৈরি করুন।", + "LabelToolsSplitM4b": "M4B কে MP3 তে বিভক্ত করুন", + "LabelToolsSplitM4bDescription": "এমবেডেড মেটাডেটা, কভার ইমেজ এবং অধ্যায় সহ অধ্যায় দ্বারা একটি M4B বিভক্ত থেকে MP3 তৈরি করুন।", + "LabelTotalDuration": "মোট সময়কাল", + "LabelTotalTimeListened": "মোট সময় শোনা", + "LabelTrackFromFilename": "ফাইলের নাম থেকে ট্র্যাক করুন", + "LabelTrackFromMetadata": "মেটাডেটা থেকে ট্র্যাক করুন", + "LabelTracks": "ট্র্যাকস", + "LabelTracksMultiTrack": "মাল্টি-ট্র্যাক", + "LabelTracksNone": "কোন ট্র্যাক নেই", + "LabelTracksSingleTrack": "একক-ট্র্যাক", + "LabelType": "টাইপ", + "LabelUnabridged": "অসংলগ্ন", + "LabelUndo": "পূর্বাবস্থা", + "LabelUnknown": "অজানা", + "LabelUpdateCover": "কভার আপডেট করুন", + "LabelUpdateCoverHelp": "একটি মিল থাকা অবস্থায় নির্বাচিত বইগুলির বিদ্যমান কভারগুলি ওভাররাইট করার অনুমতি দিন", + "LabelUpdatedAt": "আপডেট করা হয়েছে", + "LabelUpdateDetails": "বিশদ আপডেট করুন", + "LabelUpdateDetailsHelp": "একটি মিল থাকা অবস্থায় নির্বাচিত বইগুলির বিদ্যমান বিবরণ ওভাররাইট করার অনুমতি দিন", + "LabelUploaderDragAndDrop": "ফাইল বা ফোল্ডার টেনে আনুন এবং ফেলে দিন", + "LabelUploaderDropFiles": "ফাইলগুলো ফেলে দিন", + "LabelUploaderItemFetchMetadataHelp": "স্বয়ংক্রিয়ভাবে শিরোনাম, লেখক এবং সিরিজ আনুন", + "LabelUseChapterTrack": "অধ্যায় ট্র্যাক ব্যবহার করুন", + "LabelUseFullTrack": "সম্পূর্ণ ট্র্যাক ব্যবহার করুন", + "LabelUser": "ব্যবহারকারী", + "LabelUsername": "ব্যবহারকারীর নাম", + "LabelValue": "মান", + "LabelVersion": "সংস্করণ", + "LabelViewBookmarks": "বুকমার্ক দেখুন", + "LabelViewChapters": "অধ্যায় দেখুন", + "LabelViewQueue": "প্লেয়ার সারি দেখুন", + "LabelVolume": "ভলিউম", + "LabelWeekdaysToRun": "চলতে হবে সপ্তাহের দিন", + "LabelYearReviewHide": "পর্যালোচনার বছর লুকান", + "LabelYearReviewShow": "পর্যালোচনার বছর দেখুন", + "LabelYourAudiobookDuration": "আপনার অডিওবুকের সময়কাল", + "LabelYourBookmarks": "আপনার বুকমার্কস", + "LabelYourPlaylists": "আপনার প্লেলিস্ট", + "LabelYourProgress": "আপনার অগ্রগতি", + "MessageAddToPlayerQueue": "প্লেয়ার সারিতে যোগ করুন", + "MessageAppriseDescription": "এই বৈশিষ্ট্যটি ব্যবহার করার জন্য আপনাকে <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</-এর একটি উদাহরণ থাকতে হবে a> চলমান বা একটি এপিআই যা সেই একই অনুরোধগুলি পরিচালনা করবে৷ <br /> বিজ্ঞপ্তি পাঠানোর জন্য Apprise API Url সম্পূর্ণ URL পাথ হওয়া উচিত, যেমন, যদি আপনার API উদাহরণ <code>http://192.168 এ পরিবেশিত হয়৷ 1.1:8337</code> তারপর আপনি <code>http://192.168.1.1:8337/notify</code> লিখবেন।", + "MessageBackupsDescription": "ব্যাকআপের মধ্যে রয়েছে ব্যবহারকারী, ব্যবহারকারীর অগ্রগতি, লাইব্রেরি আইটেমের বিবরণ, সার্ভার সেটিংস এবং <code>/metadata/items</code> & <code>/metadata/authors</code>-এ সংরক্ষিত ছবি। ব্যাকআপগুলি <strong> আপনার লাইব্রেরি ফোল্ডারে সঞ্চিত কোনো ফাইল >অন্তর্ভুক্ত করবেন না</strong>।", + "MessageBatchQuickMatchDescription": "কুইক ম্যাচ নির্বাচিত আইটেমগুলির জন্য অনুপস্থিত কভার এবং মেটাডেটা যোগ করার চেষ্টা করবে। বিদ্যমান কভার এবং/অথবা মেটাডেটা ওভাররাইট করার জন্য দ্রুত ম্যাচকে অনুমতি দিতে নীচের বিকল্পগুলি সক্ষম করুন।", + "MessageBookshelfNoCollections": "আপনি এখনও কোনো সংগ্রহ করেননি", + "MessageBookshelfNoResultsForFilter": "ফিল্টার \"{0}: {1}\" এর জন্য কোন ফলাফল নেই", + "MessageBookshelfNoRSSFeeds": "কোনও RSS ফিড খোলা নেই", + "MessageBookshelfNoSeries": "আপনার কোনো সিরিজ নেই", + "MessageChapterEndIsAfter": "অধ্যায়ের সমাপ্তি আপনার অডিওবুকের শেষে", + "MessageChapterErrorFirstNotZero": "প্রথম অধ্যায় 0 এ শুরু হতে হবে", + "MessageChapterErrorStartGteDuration": "অবৈধ শুরুর সময় অবশ্যই অডিওবুকের সময়কালের কম হতে হবে", + "MessageChapterErrorStartLtPrev": "অবৈধ শুরুর সময় অবশ্যই আগের অধ্যায় শুরুর সময়ের চেয়ে বেশি বা সমান হতে হবে", + "MessageChapterStartIsAfter": "আপনার অডিওবুক শেষ হওয়ার পরে অধ্যায় শুরু হয়", + "MessageCeckingCron": "ক্রন পরীক্ষা করা হচ্ছে...", + "MessageConfirmCloseFeed": "আপনি কি নিশ্চিত যে আপনি এই ফিডটি বন্ধ করতে চান?", + "MessageConfirmDeleteBackup": "আপনি কি নিশ্চিত যে আপনি {0} এর ব্যাকআপ মুছে ফেলতে চান?", + "MessageConfirmDeleteFile": "এটি আপনার ফাইল সিস্টেম থেকে ফাইলটি মুছে দেবে। আপনি কি নিশ্চিত?", + "MessageConfirmDeleteLibrary": "আপনি কি নিশ্চিত যে আপনি স্থায়ীভাবে লাইব্রেরি \"{0}\" মুছে ফেলতে চান?", + "MessageConfirmDeleteLibraryItem": "এটি ডাটাবেস এবং আপনার ফাইল সিস্টেম থেকে লাইব্রেরি আইটেমটি মুছে ফেলবে। আপনি কি নিশ্চিত?", + "MessageConfirmDeleteLibraryItems": "এটি ডাটাবেস এবং আপনার ফাইল সিস্টেম থেকে {0}টি লাইব্রেরি আইটেম মুছে ফেলবে। আপনি কি নিশ্চিত?", + "MessageConfirmDeleteSession": "আপনি কি নিশ্চিত আপনি এই অধিবেশন মুছে দিতে চান?", + "MessageConfirmForceReScan": "আপনি কি নিশ্চিত যে আপনি জোর করে পুনরায় স্ক্যান করতে চান?", + "MessageConfirmMarkAllEpisodesFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্ব সমাপ্ত হিসাবে চিহ্নিত করতে চান?", + "MessageConfirmMarkAllEpisodesNotFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্বকে শেষ হয়নি বলে চিহ্নিত করতে চান?", + "MessageConfirmMarkSeriesFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে সমাপ্ত হিসাবে চিহ্নিত করতে চান?", + "MessageConfirmMarkSeriesNotFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে শেষ হয়নি বলে চিহ্নিত করতে চান?", + "MessageConfirmQuickEmbed": "সতর্কতা! দ্রুত এম্বেড আপনার অডিও ফাইলের ব্যাকআপ করবে না। নিশ্চিত করুন যে আপনার অডিও ফাইলগুলির একটি ব্যাকআপ আছে। <br><br>আপনি কি চালিয়ে যেতে চান?", + "MessageConfirmRemoveAllChapters": "আপনি কি নিশ্চিত যে আপনি সমস্ত অধ্যায় সরাতে চান?", + "MessageConfirmRemoveAuthor": "আপনি কি নিশ্চিত যে আপনি লেখক \"{0}\" অপসারণ করতে চান?", + "MessageConfirmRemoveCollection": "আপনি কি নিশ্চিত যে আপনি সংগ্রহ \"{0}\" সরাতে চান?", + "MessageConfirmRemoveEpisode": "আপনি কি নিশ্চিত আপনি \"{0}\" পর্বটি সরাতে চান?", + "MessageConfirmRemoveEpisodes": "আপনি কি নিশ্চিত যে আপনি {0}টি পর্ব সরাতে চান?", + "MessageConfirmRemoveListeningSessions": "আপনি কি নিশ্চিত যে আপনি {0}টি শোনার সেশন সরাতে চান?", + "MessageConfirmRemoveNarrator": "আপনি কি \"{0}\" বর্ণনাকারীকে সরানোর বিষয়ে নিশ্চিত?", + "MessageConfirmRemovePlaylist": "আপনি কি নিশ্চিত যে আপনি আপনার প্লেলিস্ট \"{0}\" সরাতে চান?", + "MessageConfirmRenameGenre": "আপনি কি নিশ্চিত যে আপনি সমস্ত আইটেমের জন্য \"{0}\" ধারার নাম পরিবর্তন করে \"{1}\" করতে চান?", + "MessageConfirmRenameGenreMergeNote": "দ্রষ্টব্য: এই ধারাটি আগে থেকেই বিদ্যমান তাই সেগুলিকে একত্রিত করা হবে।", + "MessageConfirmRenameGenreWarning": "সতর্কতা! একটি ভিন্ন কেসিং সহ একটি অনুরূপ ধারা ইতিমধ্যেই বিদ্যমান \"{0}\"।", + "MessageConfirmRenameTag": "আপনি কি সব আইটেমের জন্য \"{0}\" ট্যাগের নাম পরিবর্তন করে \"{1}\" করার বিষয়ে নিশ্চিত?", + "MessageConfirmRenameTagMergeNote": "দ্রষ্টব্য: এই ট্যাগটি আগে থেকেই বিদ্যমান তাই সেগুলিকে একত্র করা হবে।", + "MessageConfirmRenameTagWarning": "সতর্কতা! একটি ভিন্ন কেসিং সহ একটি অনুরূপ ট্যাগ ইতিমধ্যেই বিদ্যমান \"{0}\"।", + "MessageConfirmReScanLibraryItems": "আপনি কি নিশ্চিত যে আপনি {0}টি আইটেম পুনরায় স্ক্যান করতে চান?", + "MessageConfirmSendEbookToDevice": "আপনি কি নিশ্চিত যে আপনি \"{2}\" ডিভাইসে {0} ইবুক \"{1}\" পাঠাতে চান?", + "MessageDownloadingEpisode": "ডাউনলোডিং পর্ব", + "MessageDragFilesIntoTrackOrder": "সঠিক ট্র্যাক অর্ডারে ফাইল টেনে আনুন", + "MessageEmbedFinished": "এম্বেড করা শেষ!", + "MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ", + "MessageFeedURLWillBe": "ফিড URL হবে {0}", + "MessageFetching": "আনয় হচ্ছে...", + "MessageForceReScanDescription": "সকল ফাইল আবার নতুন স্ক্যানের মত স্ক্যান করবে। অডিও ফাইল ID3 ট্যাগ, OPF ফাইল, এবং টেক্সট ফাইলগুলি নতুন হিসাবে স্ক্যান করা হবে।", + "MessageImportantNotice": "গুরুত্বপূর্ণ বিজ্ঞপ্তি!", + "MessageInsertChapterBelow": "নীচে অধ্যায় ঢোকান", + "MessageItemsSelected": "{0}টি আইটেম নির্বাচিত", + "MessageItemsUpdated": "{0}টি আইটেম আপডেট করা হয়েছে", + "MessageJoinUsOn": "আমাদের সাথে যোগ দিন", + "MessageListeningSessionsInTheLastYear": "গত বছরে {0}টি শোনার সেশন", + "MessageLoading": "লোড হচ্ছে...", + "MessageLoadingFolders": "ফোল্ডার লোড হচ্ছে...", + "MessageM4BFailed": "M4B ব্যর্থ!", + "MessageM4BFinished": "M4B সমাপ্ত!", + "MessageMapChapterTitles": "টাইমস্ট্যাম্প সামঞ্জস্য না করে আপনার বিদ্যমান অডিওবুক অধ্যায়গুলিতে অধ্যায়ের শিরোনাম ম্যাপ করুন", + "MessageMarkAllEpisodesFinished": "সমস্ত পর্ব সমাপ্ত চিহ্নিত করুন", + "MessageMarkAllEpisodesNotFinished": "সমস্ত পর্ব শেষ হয়নি চিহ্নিত করুন", + "MessageMarkAsFinished": "সমাপ্ত হিসাবে চিহ্নিত করুন", + "MessageMarkAsNotFinished": "সমাপ্ত হয়নি হিসাবে চিহ্নিত করুন", + "MessageMatchBooksDescription": "নির্বাচিত অনুসন্ধান প্রদানকারীর একটি বইয়ের সাথে লাইব্রেরিতে বই মেলানোর চেষ্টা করবে এবং খালি বিবরণ এবং কভার আর্ট পূরণ করবে। বিস্তারিত ওভাররাইট করে না।", + "MessageNoAudioTracks": "কোন অডিও ট্র্যাক নেই", + "MessageNoAuthors": "কোন লেখক নেই", + "MessageNoBackups": "কোন ব্যাকআপ নেই", + "MessageNoBookmarks": "কোন বুকমার্ক নেই", + "MessageNoChapters": "কোনও অধ্যায় নেই", + "MessageNoCollections": "কোন সংগ্রহ নেই", + "MessageNoCoversFound": "কোন কভার পাওয়া যায়নি", + "MessageNoDescription": "কোন বর্ণনা নেই", + "MessageNoDownloadsInProgress": "বর্তমানে কোনো ডাউনলোড চলছে না", + "MessageNoDownloadsQueued": "কোনও ডাউনলোড সারি নেই", + "MessageNoEpisodeMatchesFound": "কোন পর্বের মিল পাওয়া যায়নি", + "MessageNoEpisodes": "কোন পর্ব নেই", + "MessageNoFoldersAvailable": "কোন ফোল্ডার উপলব্ধ নেই", + "MessageNoGenres": "কোন ধরন নেই", + "MessageNoIssues": "কোন সমস্যা নেই", + "MessageNoItems": "কোন আইটেম নেই", + "MessageNoItemsFound": "কোন আইটেম পাওয়া যায়নি", + "MessageNoListeningSessions": "কোনও শোনার সেশন নেই", + "MessageNoLogs": "কোনও লগ নেই", + "MessageNoMediaProgress": "মিডিয়া অগ্রগতি নেই", + "MessageNoNotifications": "কোনো বিজ্ঞপ্তি নেই", + "MessageNoPodcastsFound": "কোন পডকাস্ট পাওয়া যায়নি", + "MessageNoResults": "কোন ফলাফল নেই", + "MessageNoSearchResultsFor": "\"{0}\" এর জন্য কোন অনুসন্ধান ফলাফল নেই", + "MessageNoSeries": "কোন সিরিজ নেই", + "MessageNoTags": "কোন ট্যাগ নেই", + "MessageNoTasksRunning": "কোন টাস্ক চলছে না", + "MessageNotYetImplemented": "এখনও বাস্তবায়িত হয়নি", + "MessageNoUpdateNecessary": "কোন আপডেটের প্রয়োজন নেই", + "MessageNoUpdatesWereNecessary": "কোন আপডেটের প্রয়োজন ছিল না", + "MessageNoUserPlaylists": "আপনার কোনো প্লেলিস্ট নেই", + "MessageOr": "বা", + "MessagePauseChapter": "পজ অধ্যায় প্লেব্যাক", + "MessagePlayChapter": "অধ্যায়ের শুরুতে শুনুন", + "MessagePlaylistCreateFromCollection": "সংগ্রহ থেকে প্লেলিস্ট তৈরি করুন", + "MessagePodcastHasNoRSSFeedForMatching": "পডকাস্টের সাথে মিলের জন্য ব্যবহার করার জন্য কোন RSS ফিড ইউআরএল নেই", + "MessageQuickMatchDescription": "খালি আইটেমের বিশদ বিবরণ এবং '{0}' থেকে প্রথম ম্যাচের ফলাফলের সাথে কভার করুন। সার্ভার সেটিং সক্ষম না থাকলে বিশদ ওভাররাইট করে না।", + "MessageRemoveChapter": "অধ্যায় সরান", + "MessageRemoveEpisodes": "{0}টি পর্ব(গুলি) সরান", + "MessageRemoveFromPlayerQueue": "প্লেয়ার সারি থেকে সরান", + "MessageRemoveUserWarning": "আপনি কি নিশ্চিত আপনি স্থায়ীভাবে ব্যবহারকারী \"{0}\" মুছে ফেলতে চান?", + "MessageReportBugsAndContribute": "বাগ রিপোর্ট করুন, বৈশিষ্ট্যের অনুরোধ করুন এবং এতে অবদান রাখুন", + "MessageResetChaptersConfirm": "আপনি কি নিশ্চিত যে আপনি অধ্যায়গুলি পুনরায় সেট করতে চান এবং আপনার করা পরিবর্তনগুলি পূর্বাবস্থায় ফেরাতে চান?", + "MessageRestoreBackupConfirm": "আপনি কি নিশ্চিত যে আপনি তৈরি করা ব্যাকআপ পুনরুদ্ধার করতে চান", + "MessageRestoreBackupWarning": "একটি ব্যাকআপ পুনরুদ্ধার করা হলে তা /config-এ অবস্থিত সমগ্র ডাটাবেস ওভাররাইট করবে এবং /metadata/items & /metadata/authors-এ থাকা ছবিগুলিকে কভার করবে৷<br /><br />ব্যাকআপগুলি আপনার লাইব্রেরি ফোল্ডারে কোনো ফাইল পরিবর্তন করে না৷ আপনি যদি আপনার লাইব্রেরি ফোল্ডারে কভার আর্ট এবং মেটাডেটা সংরক্ষণ করতে সার্ভার সেটিংস সক্ষম করে থাকেন তবে সেগুলি ব্যাক আপ বা ওভাররাইট করা হয় না৷<br /><br />আপনার সার্ভার ব্যবহারকারী সমস্ত ক্লায়েন্ট স্বয়ংক্রিয়ভাবে রিফ্রেশ হবে৷", + "MessageSearchResultsFor": "এর জন্য অনুসন্ধান ফলাফল", + "MessageSelected": "{0}টি নির্বাচিত", + "MessageServerCouldNotBeReached": "সার্ভারে পৌঁছানো যায়নি", + "MessageSetChaptersFromTracksDescription": "প্রতিটি অডিও ফাইলকে অধ্যায় হিসেবে ব্যবহার করে অধ্যায় সেট করুন এবং অডিও ফাইলের নাম হিসেবে অধ্যায়ের শিরোনাম করুন", + "MessageStartPlaybackAtTime": "\"{0}\" এর জন্য {1} এ প্লেব্যাক শুরু করবেন?", + "MessageThinking": "চিন্তা করছি...", + "MessageUploaderItemFailed": "আপলোড করতে ব্যর্থ", + "MessageUploaderItemSuccess": "সফলভাবে আপলোড হয়েছে!", + "MessageUploading": "আপলোড হচ্ছে...", + "MessageValidCronExpression": "বৈধ ক্রোন এক্সপ্রেশন", + "MessageWatcherIsDisabledGlobally": "সার্ভার সেটিংসে বিশ্বব্যাপী প্রহরী অক্ষম করা হয়েছে", + "MessageXLibraryIsEmpty": "{0} লাইব্রেরি খালি!", + "MessageYourAudiobookDurationIsLonger": "আপনার অডিওবুকের সময়কাল পাওয়া সময়ের চেয়ে বেশি", + "MessageYourAudiobookDurationIsShorter": "আপনার অডিওবুকের সময়কাল পাওয়া সময়ের চেয়ে কম", + "NoteChangeRootPassword": "রুট ব্যবহারকারীই একমাত্র ব্যবহারকারী যার একটি খালি পাসওয়ার্ড থাকতে পারে", + "NoteChapterEditorTimes": "দ্রষ্টব্য: প্রথম অধ্যায়ের শুরুর সময় অবশ্যই 0:00 এ থাকতে হবে এবং শেষ অধ্যায়ের শুরুর সময়টি এই অডিওবুকের সময়কাল অতিক্রম করতে পারবে না।", + "NoteFolderPicker": "দ্রষ্টব্য: ইতিমধ্যে ম্যাপ করা ফোল্ডারগুলি দেখানো হবে না", + "NoteRSSFeedPodcastAppsHttps": "সতর্কতা: বেশিরভাগ পডকাস্ট অ্যাপের জন্য প্রয়োজন হবে RSS ফিড URL যেটি HTTPS ব্যবহার করছে", + "NoteRSSFeedPodcastAppsPubDate": "সতর্কতা: আপনার 1 বা তার বেশি পর্বের একটি পাব তারিখ নেই। কিছু পডকাস্ট অ্যাপের এটি প্রয়োজন।", + "NoteUploaderFoldersWithMediaFiles": "মিডিয়া ফাইল সহ ফোল্ডারগুলি আলাদা লাইব্রেরি আইটেম হিসাবে পরিচালনা করা হবে।", + "NoteUploaderOnlyAudioFiles": "যদি শুধুমাত্র অডিও ফাইল আপলোড করা হয় তবে প্রতিটি অডিও ফাইল একটি পৃথক অডিওবুক হিসাবে পরিচালনা করা হবে।", + "NoteUploaderUnsupportedFiles": "অসমর্থিত ফাইলগুলি উপেক্ষা করা হয়। একটি ফোল্ডার বেছে নেওয়া বা ফেলে দেওয়ার সময়, আইটেম ফোল্ডারে নেই এমন অন্যান্য ফাইলগুলি উপেক্ষা করা হয়।", + "PlaceholderNewCollection": "নতুন সংগ্রহের নাম", + "PlaceholderNewFolderPath": "নতুন ফোল্ডার পথ", + "PlaceholderNewPlaylist": "নতুন প্লেলিস্টের নাম", + "PlaceholderSearch": "অনুসন্ধান..", + "PlaceholderSearchEpisode": "অনুসন্ধান পর্ব..", + "ToastAccountUpdateFailed": "অ্যাকাউন্ট আপডেট করতে ব্যর্থ", + "ToastAccountUpdateSuccess": "অ্যাকাউন্ট আপডেট করা হয়েছে", + "ToastAuthorImageRemoveFailed": "ছবি সরাতে ব্যর্থ", + "ToastAuthorImageRemoveSuccess": "লেখকের ছবি সরানো হয়েছে", + "ToastAuthorUpdateFailed": "লেখক আপডেট করতে ব্যর্থ", + "ToastAuthorUpdateMerged": "লেখক একত্রিত হয়েছে", + "ToastAuthorUpdateSuccess": "লেখক আপডেট করেছেন", + "ToastAuthorUpdateSuccessNoImageFound": "লেখক আপডেট করেছেন (কোন ছবি পাওয়া যায়নি)", + "ToastBackupCreateFailed": "ব্যাকআপ তৈরি করতে ব্যর্থ", + "ToastBackupCreateSuccess": "ব্যাকআপ তৈরি করা হয়েছে", + "ToastBackupDeleteFailed": "ব্যাকআপ মুছে ফেলতে ব্যর্থ", + "ToastBackupDeleteSuccess": "ব্যাকআপ মুছে ফেলা হয়েছে", + "ToastBackupRestoreFailed": "ব্যাকআপ পুনরুদ্ধার করতে ব্যর্থ", + "ToastBackupUploadFailed": "ব্যাকআপ আপলোড করতে ব্যর্থ", + "ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে", + "ToastBatchUpdateFailed": "ব্যাচ আপডেট ব্যর্থ হয়েছে", + "ToastBatchUpdateSuccess": "ব্যাচ আপডেট সাফল্য", + "ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ", + "ToastBookmarkCreateSuccess": "বুকমার্ক যোগ করা হয়েছে", + "ToastBookmarkRemoveFailed": "বুকমার্ক সরাতে ব্যর্থ", + "ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে", + "ToastBookmarkUpdateFailed": "বুকমার্ক আপডেট করতে ব্যর্থ", + "ToastBookmarkUpdateSuccess": "বুকমার্ক আপডেট করা হয়েছে", + "ToastChaptersHaveErrors": "অধ্যায়ে ত্রুটি আছে", + "ToastChaptersMustHaveTitles": "অধ্যায়ের শিরোনাম থাকতে হবে", + "ToastCollectionItemsRemoveFailed": "সংগ্রহ থেকে আইটেম(গুলি) সরাতে ব্যর্থ", + "ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে", + "ToastCollectionRemoveFailed": "সংগ্রহ সরাতে ব্যর্থ", + "ToastCollectionRemoveSuccess": "সংগ্রহ সরানো হয়েছে", + "ToastCollectionUpdateFailed": "সংগ্রহ আপডেট করতে ব্যর্থ", + "ToastCollectionUpdateSuccess": "সংগ্রহ আপডেট করা হয়েছে", + "ToastItemCoverUpdateFailed": "আইটেম কভার আপডেট করতে ব্যর্থ হয়েছে", + "ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে", + "ToastItemDetailsUpdateFailed": "আইটেমের বিবরণ আপডেট করতে ব্যর্থ", + "ToastItemDetailsUpdateSuccess": "আইটেমের বিবরণ আপডেট করা হয়েছে", + "ToastItemDetailsUpdateUnneeded": "আইটেমের বিবরণের জন্য কোন আপডেটের প্রয়োজন নেই", + "ToastItemMarkedAsFinishedFailed": "সমাপ্ত হিসাবে চিহ্নিত করতে ব্যর্থ", + "ToastItemMarkedAsFinishedSuccess": "আইটেম সমাপ্ত হিসাবে চিহ্নিত", + "ToastItemMarkedAsNotFinishedFailed": "সমাপ্ত হয়নি হিসাবে চিহ্নিত করতে ব্যর্থ", + "ToastItemMarkedAsNotFinishedSuccess": "আইটেম সমাপ্ত হয়নি বলে চিহ্নিত", + "ToastLibraryCreateFailed": "লাইব্রেরি তৈরি করতে ব্যর্থ", + "ToastLibraryCreateSuccess": "লাইব্রেরি \"{0}\" তৈরি করা হয়েছে", + "ToastLibraryDeleteFailed": "লাইব্রেরি মুছে ফেলতে ব্যর্থ", + "ToastLibraryDeleteSuccess": "লাইব্রেরি মুছে ফেলা হয়েছে", + "ToastLibraryScanFailedToStart": "স্ক্যান শুরু করতে ব্যর্থ", + "ToastLibraryScanStarted": "লাইব্রেরি স্ক্যান শুরু হয়েছে", + "ToastLibraryUpdateFailed": "লাইব্রেরি আপডেট করতে ব্যর্থ", + "ToastLibraryUpdateSuccess": "লাইব্রেরি \"{0}\" আপডেট করা হয়েছে", + "ToastPlaylistCreateFailed": "প্লেলিস্ট তৈরি করতে ব্যর্থ", + "ToastPlaylistCreateSuccess": "প্লেলিস্ট তৈরি করা হয়েছে", + "ToastPlaylistRemoveFailed": "প্লেলিস্ট সরাতে ব্যর্থ", + "ToastPlaylistRemoveSuccess": "প্লেলিস্ট সরানো হয়েছে", + "ToastPlaylistUpdateFailed": "প্লেলিস্ট আপডেট করতে ব্যর্থ", + "ToastPlaylistUpdateSuccess": "প্লেলিস্ট আপডেট করা হয়েছে", + "ToastPodcastCreateFailed": "পডকাস্ট তৈরি করতে ব্যর্থ", + "ToastPodcastCreateSuccess": "পডকাস্ট সফলভাবে তৈরি করা হয়েছে", + "ToastRemoveItemFromCollectionFailed": "সংগ্রহ থেকে আইটেম সরাতে ব্যর্থ", + "ToastRemoveItemFromCollectionSuccess": "সংগ্রহ থেকে আইটেম সরানো হয়েছে", + "ToastRSSFeedCloseFailed": "RSS ফিড বন্ধ করতে ব্যর্থ", + "ToastRSSFeedCloseSuccess": "RSS ফিড বন্ধ", + "ToastSendEbookToDeviceFailed": "ডিভাইসে ইবুক পাঠাতে ব্যর্থ", + "ToastSendEbookToDeviceSuccess": "ইবুক \"{0}\" ডিভাইসে পাঠানো হয়েছে", + "ToastSeriesUpdateFailed": "সিরিজ আপডেট ব্যর্থ হয়েছে", + "ToastSeriesUpdateSuccess": "সিরিজ আপডেট সাফল্য", + "ToastSessionDeleteFailed": "সেশন মুছে ফেলতে ব্যর্থ", + "ToastSessionDeleteSuccess": "সেশন মুছে ফেলা হয়েছে", + "ToastSocketConnected": "সকেট সংযুক্ত", + "ToastSocketDisconnected": "সকেট সংযোগ বিচ্ছিন্ন", + "ToastSocketFailedToConnect": "সকেট সংযোগ করতে ব্যর্থ হয়েছে", + "ToastUserDeleteFailed": "ব্যবহারকারী মুছতে ব্যর্থ", + "ToastUserDeleteSuccess": "ব্যবহারকারী মুছে ফেলা হয়েছে" +} \ No newline at end of file From 36c1a8b2df9c2a60a7b807c2dacd44018e54bbd6 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 10 Apr 2024 17:23:12 -0500 Subject: [PATCH 0517/2145] Fix bn i18n string keys --- client/strings/bn.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/strings/bn.json b/client/strings/bn.json index d80a0bb1..6016f1cb 100644 --- a/client/strings/bn.json +++ b/client/strings/bn.json @@ -18,7 +18,7 @@ "ButtonChooseFiles": "ফাইল চয়ন করুন", "ButtonClearFilter": "ফিল্টার পরিষ্কার করুন", "ButtonCloseFeed": "ফিড বন্ধ করুন", - "Button Collections": "সংগ্রহ", + "ButtonCollections": "সংগ্রহ", "ButtonConfigureScanner": "স্ক্যানার কনফিগার করুন", "ButtonCreate": "তৈরি করুন", "ButtonCreateBackup": "ব্যাকআপ তৈরি করুন", @@ -53,7 +53,7 @@ "ButtonPlaying": "বাজছে", "ButtonPlaylists": "প্লেলিস্ট", "ButtonPrevious": "পূর্ববর্তী", - "ButtonPrevious Chapter": "আগের অধ্যায়", + "ButtonPreviousChapter": "আগের অধ্যায়", "ButtonPurgeAllCache": "সমস্ত ক্যাশে পরিষ্কার করুন", "ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন", "ButtonPurgeMediaProgress": "মিডিয়া ক্যাশে পরিষ্কার করুন", @@ -192,7 +192,7 @@ "LabelAccountTypeGuest": "অতিথি", "LabelAccountTypeUser": "ব্যবহারকারী", "LabelActivity": "ক্রিয়াকলাপ", - "Label Added": "যোগ করা হয়েছে", + "LabelAdded": "যোগ করা হয়েছে", "LabelAddedAt": "এতে যোগ করা হয়েছে", "LabelAddToCollection": "সংগ্রহে যোগ করুন", "LabelAddToCollectionBatch": "সংগ্রহে {0}টি বই যোগ করুন", @@ -237,7 +237,7 @@ "LabelCodec": "কোডেক", "LabelCollapseSeries": "সিরিজ সঙ্কুচিত করুন", "LabelCollection": "সংগ্রহ", - "Label Collections": "সংগ্রহ", + "LabelCollections": "সংগ্রহ", "LabelComplete": "সম্পূর্ণ", "LabelConfirmPassword": "পাসওয়ার্ড নিশ্চিত করুন", "LabelContinueListening": "শোনা চালিয়ে যান", @@ -583,7 +583,7 @@ "MessageChapterErrorStartGteDuration": "অবৈধ শুরুর সময় অবশ্যই অডিওবুকের সময়কালের কম হতে হবে", "MessageChapterErrorStartLtPrev": "অবৈধ শুরুর সময় অবশ্যই আগের অধ্যায় শুরুর সময়ের চেয়ে বেশি বা সমান হতে হবে", "MessageChapterStartIsAfter": "আপনার অডিওবুক শেষ হওয়ার পরে অধ্যায় শুরু হয়", - "MessageCeckingCron": "ক্রন পরীক্ষা করা হচ্ছে...", + "MessageCheckingCron": "ক্রন পরীক্ষা করা হচ্ছে...", "MessageConfirmCloseFeed": "আপনি কি নিশ্চিত যে আপনি এই ফিডটি বন্ধ করতে চান?", "MessageConfirmDeleteBackup": "আপনি কি নিশ্চিত যে আপনি {0} এর ব্যাকআপ মুছে ফেলতে চান?", "MessageConfirmDeleteFile": "এটি আপনার ফাইল সিস্টেম থেকে ফাইলটি মুছে দেবে। আপনি কি নিশ্চিত?", From df9da095ef93088f355a4f1e00b38ee522ca7bbe Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 10 Apr 2024 17:27:38 -0500 Subject: [PATCH 0518/2145] Map i18n strings to uk.json --- client/strings/pt-br.json | 4 ++-- client/strings/uk.json | 3 +++ client/strings/zh-cn.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index c97a565d..b3535071 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -385,7 +385,7 @@ "LabelNotStarted": "Não iniciado", "LabelNumberOfBooks": "Número de Livros", "LabelNumberOfEpisodes": "# de Episódios", - "LabelOpenIDAdvancedPermsClaimDescription": "Nome do claim OpenID contendo as permissões avançadas para ações do usuário na aplicação para serem aplicadas aos perfis não-administradores (<b>se configurados</b>). Se o claim não estiver presente na resposta, acesso ao ABS será negado. Se apenas uma opção estiver ausente, ela será tratada como <code>false</code>. Garanta que o claim do provedor de identidade segue a estrutura esperada:", + "LabelOpenIDAdvancedPermsClaimDescription": "Nome do claim OpenID contendo as permissões avançadas para ações do usuário na aplicação para serem aplicadas aos perfis não-administradores (<b>se configurados</b>). Se o claim não estiver presente na resposta, acesso ao ABS será negado. Se apenas uma opção estiver ausente, ela será tratada como <code>false</code>. Garanta que o claim do provedor de identidade segue a estrutura esperada:", "LabelOpenIDClaims": "Deixe as opções a seguir em branco para desativar a atribuição de grupos e permissões avançadas; nesse caso, o grupo 'Usuário' será atribuído automaticamente.", "LabelOpenIDGroupClaimDescription": "Nome do claim OpenID contendo a lista de grupos do usuário, normalmente chamada de <code>groups</code>. <b>Se configurada</b>, a aplicação atribuirá automaticamente os perfis com base na participação do usuário nos grupos, contanto que os nomes desses grupos no claim, sem distinção entre maiúsculas e minúsculas, sejam 'admin', 'user' ou 'guest'. O claim deve conter uma lista e, se o usuário pertencer a múltiplos grupos, a aplicação atribuirá o perfil correspondendo ao maior nível de acesso. Se não houver correspondência a qualquer grupo, o acesso será negado.", "LabelOpenRSSFeed": "Abrir Feed RSS", @@ -779,4 +779,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} +} \ No newline at end of file diff --git a/client/strings/uk.json b/client/strings/uk.json index 7c0d4476..e0c98cc0 100644 --- a/client/strings/uk.json +++ b/client/strings/uk.json @@ -385,6 +385,9 @@ "LabelNotStarted": "Не розпочато", "LabelNumberOfBooks": "Кількість книг", "LabelNumberOfEpisodes": "Кількість епізодів", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenRSSFeed": "Відкрити RSS-канал", "LabelOverwrite": "Перезаписати", "LabelPassword": "Пароль", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 4ff8d74b..64dd606a 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -779,4 +779,4 @@ "ToastSocketFailedToConnect": "网络连接失败", "ToastUserDeleteFailed": "删除用户失败", "ToastUserDeleteSuccess": "用户已删除" -} +} \ No newline at end of file From 75007bb371ff7403fdb895268e1cffc7d3c01695 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Thu, 11 Apr 2024 01:28:40 +0000 Subject: [PATCH 0519/2145] Fix: i18n-integration not running on PRs --- .github/workflows/i18n-integration.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/i18n-integration.yml b/.github/workflows/i18n-integration.yml index cee07b20..87dcd195 100644 --- a/.github/workflows/i18n-integration.yml +++ b/.github/workflows/i18n-integration.yml @@ -1,6 +1,9 @@ name: Verify all i18n files are alphabetized on: + pull_request: + paths: + - client/strings/** # Should only check if any strings changed push: paths: - client/strings/** # Should only check if any strings changed From 6ed6fff6bdbb1f225e2f031b2597095d1456372a Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Thu, 11 Apr 2024 01:29:00 +0000 Subject: [PATCH 0520/2145] Update i18n workflow to 1.2.0 --- .github/workflows/i18n-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/i18n-integration.yml b/.github/workflows/i18n-integration.yml index 87dcd195..12d82c3e 100644 --- a/.github/workflows/i18n-integration.yml +++ b/.github/workflows/i18n-integration.yml @@ -25,6 +25,6 @@ jobs: # The only argument is the `directory`, which is where the i18n files are # stored. - name: Run Update JSON Files action - uses: audiobookshelf/audiobookshelf-i18n-updater@v1.1.1 + uses: audiobookshelf/audiobookshelf-i18n-updater@v1.2.0 with: directory: "client/strings/" # Adjust the directory path as needed From 9a0b8de354cb6cdcfcdb6691579ae81740fc41dc Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Thu, 11 Apr 2024 01:31:22 +0000 Subject: [PATCH 0521/2145] Example: bad key in `es.json` --- client/strings/es.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/strings/es.json b/client/strings/es.json index 1b0bfbec..a7080739 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -8,6 +8,7 @@ "ButtonAddYourFirstLibrary": "Crea tu Primera Biblioteca", "ButtonApply": "Aplicar", "ButtonApplyChapters": "Aplicar Capítulos", + "Button ApplyChapters": "Aplicar Capítulos", "ButtonAuthors": "Autores", "ButtonBrowseForFolder": "Buscar por Carpeta", "ButtonCancel": "Cancelar", From f702358bbdecfcb822493419c407fe5eb24caaec Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Thu, 11 Apr 2024 01:33:38 +0000 Subject: [PATCH 0522/2145] Example: missing key in `es.json` --- client/strings/es.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/es.json b/client/strings/es.json index a7080739..a725cd85 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -8,7 +8,6 @@ "ButtonAddYourFirstLibrary": "Crea tu Primera Biblioteca", "ButtonApply": "Aplicar", "ButtonApplyChapters": "Aplicar Capítulos", - "Button ApplyChapters": "Aplicar Capítulos", "ButtonAuthors": "Autores", "ButtonBrowseForFolder": "Buscar por Carpeta", "ButtonCancel": "Cancelar", @@ -779,5 +778,6 @@ "ToastSocketDisconnected": "Socket desconectado", "ToastSocketFailedToConnect": "Error al conectar al Socket", "ToastUserDeleteFailed": "Error al eliminar el usuario", - "ToastUserDeleteSuccess": "Usuario eliminado" + "ToastUserDeleteSuccess": "Usuario eliminado", + "ZZZZ": "example" } \ No newline at end of file From 9e1686232bde7d3820c3973e3fbe0133b3ed9bef Mon Sep 17 00:00:00 2001 From: Nicholas Wallace <nicholaslwallace@gmail.com> Date: Thu, 11 Apr 2024 01:34:32 +0000 Subject: [PATCH 0523/2145] Example: `es.json` is fixed --- client/strings/es.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/strings/es.json b/client/strings/es.json index a725cd85..1b0bfbec 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -778,6 +778,5 @@ "ToastSocketDisconnected": "Socket desconectado", "ToastSocketFailedToConnect": "Error al conectar al Socket", "ToastUserDeleteFailed": "Error al eliminar el usuario", - "ToastUserDeleteSuccess": "Usuario eliminado", - "ZZZZ": "example" + "ToastUserDeleteSuccess": "Usuario eliminado" } \ No newline at end of file From bca49616e1f2bf5b574e2b98aea50e7a84a9837a Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Thu, 11 Apr 2024 17:29:23 -0500 Subject: [PATCH 0524/2145] Update:Podcast episode audio file ID3 tags use comment and description tag for description instead of subtitle #2843 --- server/scanner/AudioFileScanner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/scanner/AudioFileScanner.js b/server/scanner/AudioFileScanner.js index 89951025..10437bc1 100644 --- a/server/scanner/AudioFileScanner.js +++ b/server/scanner/AudioFileScanner.js @@ -378,7 +378,7 @@ class AudioFileScanner { const MetadataMapArray = [ { tag: 'tagComment', - altTag: 'tagSubtitle', + altTag: 'tagDescription', key: 'description' }, { From c4fd4ff9de3162292c3e272b2dfa2752af5a7f97 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Fri, 12 Apr 2024 17:34:10 -0500 Subject: [PATCH 0525/2145] Fix:Update metadata.json when using item metadata utils #2837 --- server/controllers/MiscController.js | 4 + server/models/LibraryItem.js | 145 +++++++++++++++++++++ server/utils/queries/libraryItemFilters.js | 6 +- 3 files changed, 154 insertions(+), 1 deletion(-) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 274affa7..8bf0c31e 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -329,6 +329,7 @@ class MiscController { await libraryItem.media.update({ tags: libraryItem.media.tags }) + await libraryItem.saveMetadataFile() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) numItemsUpdated++ @@ -370,6 +371,7 @@ class MiscController { await libraryItem.media.update({ tags: libraryItem.media.tags }) + await libraryItem.saveMetadataFile() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) numItemsUpdated++ @@ -462,6 +464,7 @@ class MiscController { await libraryItem.media.update({ genres: libraryItem.media.genres }) + await libraryItem.saveMetadataFile() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) numItemsUpdated++ @@ -503,6 +506,7 @@ class MiscController { await libraryItem.media.update({ genres: libraryItem.media.genres }) + await libraryItem.saveMetadataFile() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) numItemsUpdated++ diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 704e2f10..5a35a5d6 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -1,8 +1,12 @@ +const Path = require('path') const { DataTypes, Model } = require('sequelize') +const fsExtra = require('../libs/fsExtra') const Logger = require('../Logger') const oldLibraryItem = require('../objects/LibraryItem') const libraryFilters = require('../utils/queries/libraryFilters') const { areEquivalent } = require('../utils/index') +const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') +const LibraryFile = require('../objects/files/LibraryFile') const Book = require('./Book') const Podcast = require('./Podcast') @@ -828,6 +832,147 @@ class LibraryItem extends Model { return this[mixinMethodName](options) } + /** + * + * @returns {Promise<Book|Podcast>} + */ + getMediaExpanded() { + if (this.mediaType === 'podcast') { + return this.getMedia({ + include: [ + { + model: this.sequelize.models.podcastEpisode + } + ] + }) + } else { + return this.getMedia({ + include: [ + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + } + ], + order: [ + [this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'], + [this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'] + ] + }) + } + } + + /** + * + * @returns {Promise} + */ + async saveMetadataFile() { + let metadataPath = Path.join(global.MetadataPath, 'items', this.id) + let storeMetadataWithItem = global.ServerSettings.storeMetadataWithItem + if (storeMetadataWithItem && !this.isFile) { + metadataPath = this.path + } else { + // Make sure metadata book dir exists + storeMetadataWithItem = false + await fsExtra.ensureDir(metadataPath) + } + + const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) + + // Expanded with series, authors, podcastEpisodes + const mediaExpanded = this.media || await this.getMediaExpanded() + + let jsonObject = {} + if (this.mediaType === 'book') { + jsonObject = { + tags: mediaExpanded.tags || [], + chapters: mediaExpanded.chapters?.map(c => ({ ...c })) || [], + title: mediaExpanded.title, + subtitle: mediaExpanded.subtitle, + authors: mediaExpanded.authors.map(a => a.name), + narrators: mediaExpanded.narrators, + series: mediaExpanded.series.map(se => { + const sequence = se.bookSeries?.sequence || '' + if (!sequence) return se.name + return `${se.name} #${sequence}` + }), + genres: mediaExpanded.genres || [], + publishedYear: mediaExpanded.publishedYear, + publishedDate: mediaExpanded.publishedDate, + publisher: mediaExpanded.publisher, + description: mediaExpanded.description, + isbn: mediaExpanded.isbn, + asin: mediaExpanded.asin, + language: mediaExpanded.language, + explicit: !!mediaExpanded.explicit, + abridged: !!mediaExpanded.abridged + } + } else { + jsonObject = { + tags: mediaExpanded.tags || [], + title: mediaExpanded.title, + author: mediaExpanded.author, + description: mediaExpanded.description, + releaseDate: mediaExpanded.releaseDate, + genres: mediaExpanded.genres || [], + feedURL: mediaExpanded.feedURL, + imageURL: mediaExpanded.imageURL, + itunesPageURL: mediaExpanded.itunesPageURL, + itunesId: mediaExpanded.itunesId, + itunesArtistId: mediaExpanded.itunesArtistId, + asin: mediaExpanded.asin, + language: mediaExpanded.language, + explicit: !!mediaExpanded.explicit, + podcastType: mediaExpanded.podcastType + } + } + + + return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, 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) { + const newLibraryFile = new LibraryFile() + await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) + metadataLibraryFile = newLibraryFile.toJSON() + 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.mtime = libraryItemDirTimestamps.mtimeMs + this.ctime = libraryItemDirTimestamps.ctimeMs + let size = 0 + this.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) + this.size = size + await this.save() + } + } + + Logger.debug(`Success saving abmetadata to "${metadataFilePath}"`) + + return metadataLibraryFile + }).catch((error) => { + Logger.error(`Failed to save json file at "${metadataFilePath}"`, error) + return null + }) + } + /** * Initialize model * @param {import('../Database').sequelize} sequelize diff --git a/server/utils/queries/libraryItemFilters.js b/server/utils/queries/libraryItemFilters.js index fbc6186e..677b11c7 100644 --- a/server/utils/queries/libraryItemFilters.js +++ b/server/utils/queries/libraryItemFilters.js @@ -34,6 +34,10 @@ module.exports = { attributes: ['sequence'] } } + ], + order: [ + [Database.authorModel, Database.bookAuthorModel, 'createdAt', 'ASC'], + [Database.seriesModel, 'bookSeries', 'createdAt', 'ASC'] ] }) for (const book of booksWithTag) { @@ -68,7 +72,7 @@ module.exports = { /** * Get all library items that have genres * @param {string[]} genres - * @returns {Promise<LibraryItem[]>} + * @returns {Promise<import('../../models/LibraryItem')[]>} */ async getAllLibraryItemsWithGenres(genres) { const libraryItems = [] From 60c65008dcda0c971cdfeb82cef9465778efab3b Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Sun, 14 Apr 2024 17:19:21 -0500 Subject: [PATCH 0526/2145] Fix:Match all books only matching first 100 #2096 --- server/scanner/Scanner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 040053e4..e0bcf4fd 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -359,7 +359,7 @@ class Scanner { } offset += limit - hasMoreChunks = libraryItems.length < limit + hasMoreChunks = libraryItems.length === limit let oldLibraryItems = libraryItems.map(li => Database.libraryItemModel.getOldLibraryItem(li)) const shouldContinue = await this.matchLibraryItemsChunk(library, oldLibraryItems, libraryScan) From 0f7c99d98965fe26660d93588bcc934dbccc6324 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Mon, 15 Apr 2024 15:14:30 -0500 Subject: [PATCH 0527/2145] Fix:Retry transcode forcing AAC to handle the bad audible m4bs #2720 --- server/objects/Stream.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/objects/Stream.js b/server/objects/Stream.js index 2ee66182..f7012188 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -195,7 +195,7 @@ class Stream extends EventEmitter { var current_chunk = [] var last_seg_in_chunk = -1 - var segments = Array.from(this.segmentsCreated).sort((a, b) => a - b); + var segments = Array.from(this.segmentsCreated).sort((a, b) => a - b) var lastSegment = segments[segments.length - 1] if (lastSegment > this.furthestSegmentCreated) { this.furthestSegmentCreated = lastSegment @@ -342,7 +342,7 @@ class Stream extends EventEmitter { Logger.error('Ffmpeg Err', '"' + err.message + '"') // Temporary workaround for https://github.com/advplyr/audiobookshelf/issues/172 and https://github.com/advplyr/audiobookshelf/issues/2157 - const aacErrorMsg = 'ffmpeg exited with code 1:' + const aacErrorMsg = 'ffmpeg exited with code 1' if (audioCodec === 'copy' && this.isAACEncodable && err.message?.startsWith(aacErrorMsg)) { Logger.info(`[Stream] Re-attempting stream with AAC encode`) this.transcodeOptions.forceAAC = true From 303ef6b7c52302ffeffcfa4642f2329862c46edd Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Mon, 15 Apr 2024 23:54:56 +0300 Subject: [PATCH 0528/2145] Add Cypress to dev dependencies --- client/package-lock.json | 1072 +++++++++++++++++++++++++++++++++++++- client/package.json | 1 + 2 files changed, 1072 insertions(+), 1 deletion(-) diff --git a/client/package-lock.json b/client/package-lock.json index a3435d74..50f18acb 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -28,6 +28,7 @@ "devDependencies": { "@nuxtjs/pwa": "^3.3.5", "autoprefixer": "^10.4.7", + "cypress": "^13.7.3", "postcss": "^8.3.6", "tailwindcss": "^3.4.1" } @@ -1920,6 +1921,16 @@ "node": ">=6.9.0" } }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@csstools/cascade-layer-name-parser": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.7.tgz", @@ -2781,6 +2792,69 @@ "postcss-selector-parser": "^6.0.13" } }, + "node_modules/@cypress/request": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.10.4", + "safe-buffer": "^5.1.2", + "tough-cookie": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request/node_modules/qs": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -4209,6 +4283,18 @@ "@types/node": "*" } }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true + }, "node_modules/@types/source-list-map": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", @@ -4284,6 +4370,16 @@ "node": ">= 8" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vue/babel-helper-vue-jsx-merge-props": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz", @@ -4766,6 +4862,15 @@ "string-width": "^4.1.0" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -4847,6 +4952,26 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "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/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -4942,6 +5067,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -4967,6 +5101,15 @@ "util": "^0.10.4" } }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/assert/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", @@ -4988,6 +5131,21 @@ "node": ">=0.10.0" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, "node_modules/async-each": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", @@ -5000,6 +5158,12 @@ ], "optional": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -5067,6 +5231,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "dev": true + }, "node_modules/axios": { "version": "0.21.4", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", @@ -5267,6 +5446,15 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -5292,6 +5480,12 @@ "file-uri-to-path": "1.0.0" } }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -5532,6 +5726,15 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5723,6 +5926,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -5800,6 +6012,12 @@ } ] }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5820,6 +6038,15 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -5963,6 +6190,37 @@ "node": ">=8" } }, + "node_modules/cli-table3": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.4.tgz", + "integrity": "sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", @@ -6023,11 +6281,32 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -6679,6 +6958,167 @@ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.2.tgz", "integrity": "sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==" }, + "node_modules/cypress": { + "version": "13.7.3", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.3.tgz", + "integrity": "sha512-uoecY6FTCAuIEqLUYkTrxamDBjMHTYak/1O7jtgwboHiTnS1NaMOoR08KcTrbRZFCBvYOiS4tEkQRmsV+xcrag==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.0", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cypress/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cypress/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cypress/node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/cypress/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cypress/node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/cypress/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/cypress/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, "node_modules/d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", @@ -6688,6 +7128,18 @@ "type": "^1.0.1" } }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -6703,6 +7155,12 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", + "dev": true + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -6792,6 +7250,15 @@ "integrity": "sha512-8UWj5lNv7HD+kB0e9w77Z7TdQlbUYDVWqITLHNqFIn6khrNHv5WQo38Dcm1f6HeNyZf0U7UbPf6WeZDSdCzGDQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7006,6 +7473,16 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7125,6 +7602,19 @@ "node": ">=6.9.0" } }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -7383,6 +7873,12 @@ "es5-ext": "~0.10.14" } }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -7432,6 +7928,27 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/executable/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -7525,6 +8042,12 @@ "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, "node_modules/extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -7652,6 +8175,50 @@ "node": ">= 4" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7685,6 +8252,15 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -7903,6 +8479,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -8105,6 +8704,24 @@ "node": ">=0.10.0" } }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/git-config-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/git-config-path/-/git-config-path-2.0.0.tgz", @@ -8164,6 +8781,30 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -8884,6 +9525,20 @@ "node": ">=8.0.0" } }, + "node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -9153,6 +9808,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -9239,6 +9906,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -9272,6 +9955,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", @@ -9392,6 +10084,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -9429,6 +10139,12 @@ "node": ">=0.10.0" } }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -9479,6 +10195,12 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -9500,11 +10222,23 @@ "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==" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -9527,6 +10261,21 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -9580,6 +10329,15 @@ "launch-editor": "^2.6.1" } }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, "node_modules/libarchive.js": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/libarchive.js/-/libarchive.js-1.3.0.tgz", @@ -9606,6 +10364,48 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/listr2/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -9679,6 +10479,12 @@ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, "node_modules/lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", @@ -9701,6 +10507,71 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, + "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/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/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/lower-case": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", @@ -11226,6 +12097,12 @@ "node": ">=0.10.0" } }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -11505,6 +12382,18 @@ "worker-loader": "^3.0.7" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -12932,6 +13821,12 @@ "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==" }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -12942,6 +13837,12 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -13030,6 +13931,12 @@ "node": ">=0.4.x" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -13389,6 +14296,15 @@ "node": ">=0.10" } }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -13461,6 +14377,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -13949,6 +14871,20 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -14211,6 +15147,31 @@ "node": ">=0.10.0" } }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -14977,6 +15938,15 @@ "node": ">=8.9.0" } }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -15102,6 +16072,30 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -15141,6 +16135,24 @@ "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, "node_modules/type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", @@ -15417,6 +16429,15 @@ "node": ">=0.10.0" } }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/upath": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", @@ -15533,6 +16554,16 @@ "node": ">=8.9.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/url/node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -15586,6 +16617,15 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v-click-outside": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/v-click-outside/-/v-click-outside-3.2.0.tgz", @@ -15602,6 +16642,26 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -16964,6 +18024,16 @@ "node": ">= 6" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -16976,4 +18046,4 @@ } } } -} \ No newline at end of file +} diff --git a/client/package.json b/client/package.json index 5ce9766d..6597fc60 100644 --- a/client/package.json +++ b/client/package.json @@ -33,6 +33,7 @@ "devDependencies": { "@nuxtjs/pwa": "^3.3.5", "autoprefixer": "^10.4.7", + "cypress": "^13.7.3", "postcss": "^8.3.6", "tailwindcss": "^3.4.1" } From f5977988398dacdd021cbd8c549ccdc781aa24c3 Mon Sep 17 00:00:00 2001 From: mikiher <mikiher@gmail.com> Date: Mon, 15 Apr 2024 23:57:21 +0300 Subject: [PATCH 0529/2145] Add Cypress config and support files --- client/cypress.config.js | 10 ++++++++ client/cypress/support/commands.js | 25 ++++++++++++++++++ client/cypress/support/component-index.html | 13 ++++++++++ client/cypress/support/component.js | 28 +++++++++++++++++++++ client/nuxt.config.js | 2 ++ 5 files changed, 78 insertions(+) create mode 100644 client/cypress.config.js create mode 100644 client/cypress/support/commands.js create mode 100644 client/cypress/support/component-index.html create mode 100644 client/cypress/support/component.js diff --git a/client/cypress.config.js b/client/cypress.config.js new file mode 100644 index 00000000..6874dca6 --- /dev/null +++ b/client/cypress.config.js @@ -0,0 +1,10 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + component: { + devServer: { + framework: "nuxt", + bundler: "webpack", + }, + }, +}); diff --git a/client/cypress/support/commands.js b/client/cypress/support/commands.js new file mode 100644 index 00000000..66ea16ef --- /dev/null +++ b/client/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) \ No newline at end of file diff --git a/client/cypress/support/component-index.html b/client/cypress/support/component-index.html new file mode 100644 index 00000000..a19e8295 --- /dev/null +++ b/client/cypress/support/component-index.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width,initial-scale=1.0"> + <link rel="stylesheet" href="tailwind.compiled.css"> + <title>Components App + + +
+ + \ No newline at end of file diff --git a/client/cypress/support/component.js b/client/cypress/support/component.js new file mode 100644 index 00000000..e0b83383 --- /dev/null +++ b/client/cypress/support/component.js @@ -0,0 +1,28 @@ +// *********************************************************** +// This example support/component.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** +import '../../assets/app.css' +import './tailwind.compiled.css' +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +import { mount } from 'cypress/vue2' + +Cypress.Commands.add('mount', mount) + +// Example use: +// cy.mount(MyComponent) \ No newline at end of file diff --git a/client/nuxt.config.js b/client/nuxt.config.js index b5659086..7dde5572 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -153,4 +153,6 @@ module.exports = { * See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480) */ devServerHandlers: [], + + ignore: ["**/*.test.*", "**/*.cy.*"] } From d638a328d8e5ef0d0576582cd96a8da2917b7c63 Mon Sep 17 00:00:00 2001 From: mikiher Date: Mon, 15 Apr 2024 23:58:13 +0300 Subject: [PATCH 0530/2145] Add cypress npm scripts --- client/package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index 6597fc60..fa15318f 100644 --- a/client/package.json +++ b/client/package.json @@ -9,7 +9,10 @@ "dev2": "nuxt --hostname localhost --port 1337", "build": "nuxt build", "start": "nuxt start", - "generate": "nuxt generate" + "generate": "nuxt generate", + "test": "npm run compile-tailwind && cypress run --component --browser chrome", + "test-visually": "npm run compile-tailwind && cypress open --component --browser chrome", + "compile-tailwind": "tailwindcss -i ./assets/tailwind.css -o ./cypress/support/tailwind.compiled.css" }, "author": "advplyr", "license": "ISC", From 9e1c907591d9c7292b8f6208351275541fc0e49f Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 16 Apr 2024 00:00:35 +0300 Subject: [PATCH 0531/2145] Add NarratorCard and AuthorCard component tests --- client/components/cards/AuthorCard.cy.js | 192 +++++++++++++++++++++ client/components/cards/AuthorCard.vue | 20 +-- client/components/cards/NarratorCard.cy.js | 86 +++++++++ client/components/cards/NarratorCard.vue | 18 +- 4 files changed, 300 insertions(+), 16 deletions(-) create mode 100644 client/components/cards/AuthorCard.cy.js create mode 100644 client/components/cards/NarratorCard.cy.js diff --git a/client/components/cards/AuthorCard.cy.js b/client/components/cards/AuthorCard.cy.js new file mode 100644 index 00000000..0f78229e --- /dev/null +++ b/client/components/cards/AuthorCard.cy.js @@ -0,0 +1,192 @@ +// Import the necessary dependencies +import AuthorCard from './AuthorCard.vue' +import AuthorImage from '../covers/AuthorImage.vue' +import Tooltip from '../ui/Tooltip.vue' +import LoadingSpinner from '../widgets/LoadingSpinner.vue' + +describe('AuthorCard', () => { + const author = { + id: 1, + name: 'John Doe', + numBooks: 5 + } + + const propsData = { + author, + width: 192 * 0.8, + height: 192, + sizeMultiplier: 1, + nameBelow: false + } + + const mocks = { + $strings: { + LabelBooks: 'Books', + ButtonQuickMatch: 'Quick Match' + }, + $store : { + getters: { + 'user/getUserCanUpdate': true, + 'libraries/getLibraryProvider': () => 'audible.us' + }, + state: { + libraries: { + currentLibraryId: 'library-123' + } + } + }, + $eventBus: { + $on: () => {}, + $off: () => {}, + }, + } + + const stubs = { + 'covers-author-image': AuthorImage, + 'ui-tooltip': Tooltip, + 'widgets-loading-spinner': LoadingSpinner + } + + const mountOptions = { propsData, mocks, stubs } + + it('renders the component', () => { + cy.mount(AuthorCard, mountOptions) + + cy.get('#textInline').should('be.visible') + cy.get('#match').should('be.hidden') + cy.get('#edit').should('be.hidden') + cy.get('#nameBelow').should('be.hidden') + cy.get('#card').should(($el) => { + const width = $el.width() + const height = $el.height() + expect(width).to.be.closeTo(propsData.width, 0.01) + expect(height).to.be.closeTo(propsData.height, 0.01) + }) + }) + + it('renders the component with the author name below', () => { + const updatedPropsData = { ...propsData, nameBelow: true } + cy.mount(AuthorCard, { ...mountOptions, propsData: updatedPropsData } ) + + cy.get('#textInline').should('be.hidden') + cy.get('#match').should('be.hidden') + cy.get('#edit').should('be.hidden') + let nameBelowHeight + cy.get('#nameBelow') + .should('be.visible') + .and('have.text', 'John Doe') + .and(($el) => { + const height = $el.height() + const width = $el.width() + const sizeMultiplier = propsData.sizeMultiplier + const defaultFontSize = 16 + const defaultLineHeight = 1.5 + const fontSizeMultiplier = 0.75 + const px2 = 16 + expect(height).to.be.closeTo(defaultFontSize * fontSizeMultiplier * sizeMultiplier * defaultLineHeight, 0.01) + nameBelowHeight = height + expect(width).to.be.closeTo(propsData.width - px2, 0.01) + }) + cy.get('#card').should(($el) => { + const width = $el.width() + const height = $el.height() + const py1 = 8 + expect(width).to.be.closeTo(propsData.width, 0.01) + expect(height).to.be.closeTo(propsData.height + nameBelowHeight + py1, 0.01) + }) + + }) + + it('renders quick-match and edit buttons on mouse hover', () => { + cy.mount(AuthorCard, mountOptions ) + + // before mouseover + cy.get('#match').should('be.hidden') + cy.get('#edit').should('be.hidden') + // after mouseover + cy.get('#card').trigger('mouseover') + cy.get('#match').should('be.visible') + cy.get('#edit').should('be.visible') + // after mouseleave + cy.get('#card').trigger('mouseleave') + cy.get('#match').should('be.hidden') + cy.get('#edit').should('be.hidden') + + }) + + it('renders the component with spinner while searching', () => { + const data = () => { return { searching: true, isHovering: false } } + cy.mount(AuthorCard, { ...mountOptions, data } ) + + cy.get('#textInline').should('be.hidden') + cy.get('#match').should('be.hidden') + cy.get('#edit').should('be.hidden') + cy.get('#spinner').should('be.visible') + }) + + it ('toasts after quick match with no updates', () => { + const updatedMocks = { + ...mocks, + $axios: { + $post: cy.stub().resolves({ updated: false, author: { name: 'John Doe' } }) + }, + $toast: { + success: cy.spy().as('success'), + error: cy.spy().as('error'), + info: cy.spy().as('info') + } + } + cy.mount(AuthorCard, { ...mountOptions, mocks: updatedMocks } ) + cy.get('#card').trigger('mouseover') + cy.get('#match').click() + + cy.get("#spinner").should('be.hidden') + cy.get('@success').should('not.have.been.called') + cy.get('@error').should('not.have.been.called') + cy.get('@info').should('have.been.called') + }) + + it ('toasts after quick match with updates and no image', () => { + const updatedMocks = { + ...mocks, + $axios: { + $post: cy.stub().resolves({ updated: true, author: { name: 'John Doe' } }) + }, + $toast: { + success: cy.stub().withArgs('Author John Doe was updated (no image found)').as('success'), + error: cy.spy().as('error'), + info: cy.spy().as('info') + } + } + cy.mount(AuthorCard, { ...mountOptions, mocks: updatedMocks } ) + cy.get('#card').trigger('mouseover') + cy.get('#match').click() + + cy.get("#spinner").should('be.hidden') + cy.get('@success').should('have.been.called') + cy.get('@error').should('not.have.been.called') + cy.get('@info').should('not.have.been.called') + }) + + it ('toasts after quick match with updates including image', () => { + const updatedMocks = { + ...mocks, + $axios: { + $post: cy.stub().resolves({ updated: true, author: { name: 'John Doe', imagePath: "path/to/image" } }) + }, + $toast: { + success: cy.stub().withArgs('Author John Doe was updated').as('success'), + error: cy.spy().as('error'), + info: cy.spy().as('info') + } + } + cy.mount(AuthorCard, { ...mountOptions, mocks: updatedMocks } ) + cy.get('#card').trigger('mouseover') + cy.get('#match').click() + + cy.get("#spinner").should('be.hidden') + cy.get('@success').should('have.been.called') + cy.get('@error').should('not.have.been.called') + cy.get('@info').should('not.have.been.called') + }) +}) \ No newline at end of file diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue index fc3bc4b2..e685e942 100644 --- a/client/components/cards/AuthorCard.vue +++ b/client/components/cards/AuthorCard.vue @@ -1,35 +1,35 @@