feat: Implement webhook-based status notifications #66

* Implemented notifications for Gotify and Ntfy
* Notifications pushed on: polling started, polling retry, polling stopped on error, scrobble client scrobble failure
This commit is contained in:
FoxxMD
2023-02-24 13:19:03 -05:00
parent 0a41d32084
commit c24dc41811
24 changed files with 838 additions and 43 deletions
+359
View File
@@ -20,8 +20,10 @@
"express": "^4.17.1",
"express-session": "^1.17.2",
"formidable": "^2.1",
"gotify": "^1.1.0",
"json5": "^2.2.3",
"lastfm-node-client": "^2.2.0",
"ntfy": "^1.0.5",
"passport": "^0.6.0",
"passport-deezer": "^0.2.0",
"pony-cause": "^1.1.1",
@@ -125,6 +127,28 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@sindresorhus/is": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz",
"integrity": "sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"dependencies": {
"defer-to-connect": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@@ -165,6 +189,17 @@
"@types/node": "*"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"dependencies": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
"@types/node": "*",
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/component-emitter": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz",
@@ -248,6 +283,11 @@
"@types/node": "*"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
},
"node_modules/@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@@ -260,6 +300,14 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"node_modules/@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/mime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
@@ -292,6 +340,14 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"node_modules/@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz",
@@ -429,6 +485,16 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.1.tgz",
"integrity": "sha512-78pWJsQTceInlyaeBQeYZ/QgZeWS8hGeKiIJiDKQe3hEyBb7sEMq0K4gjx+Va6WHTYO4zI/RRl8qGRzn0YMadA==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -503,6 +569,35 @@
"node": ">= 0.8"
}
},
"node_modules/cacheable-lookup": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-2.0.1.tgz",
"integrity": "sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg==",
"dependencies": {
"@types/keyv": "^3.1.1",
"keyv": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cacheable-request": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
"integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.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",
@@ -552,6 +647,25 @@
"node": ">=12"
}
},
"node_modules/clone-response": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
"dependencies": {
"mimic-response": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/clone-response/node_modules/mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
@@ -740,6 +854,25 @@
"ms": "2.0.0"
}
},
"node_modules/decompress-response": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-5.0.0.tgz",
"integrity": "sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw==",
"dependencies": {
"mimic-response": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"engines": {
"node": ">=10"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -783,6 +916,11 @@
"node": ">=0.3.1"
}
},
"node_modules/duplexer3": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz",
"integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA=="
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -821,6 +959,14 @@
"node": ">= 0.8"
}
},
"node_modules/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==",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/engine.io": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.1.1.tgz",
@@ -1081,6 +1227,25 @@
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"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==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@@ -1157,6 +1322,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"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==",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"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",
@@ -1177,6 +1356,42 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/got": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/got/-/got-10.7.0.tgz",
"integrity": "sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg==",
"dependencies": {
"@sindresorhus/is": "^2.0.0",
"@szmarczak/http-timer": "^4.0.0",
"@types/cacheable-request": "^6.0.1",
"cacheable-lookup": "^2.0.0",
"cacheable-request": "^7.0.1",
"decompress-response": "^5.0.0",
"duplexer3": "^0.1.4",
"get-stream": "^5.0.0",
"lowercase-keys": "^2.0.0",
"mimic-response": "^2.1.0",
"p-cancelable": "^2.0.0",
"p-event": "^4.0.0",
"responselike": "^2.0.0",
"to-readable-stream": "^2.0.0",
"type-fest": "^0.10.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/gotify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/gotify/-/gotify-1.1.0.tgz",
"integrity": "sha512-f3PUh08i+1JCJuzv9CRYuIx1QOb9DoWXRvWaPttgiZEG7XtetDQVv6S9cr6suPNKkZI4ry8dqdU2qdb9AiPinw==",
"dependencies": {
"got": "10.7.0"
}
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -1215,6 +1430,11 @@
"node": ">=8"
}
},
"node_modules/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=="
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -1306,6 +1526,11 @@
"node": ">=10"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -1322,6 +1547,14 @@
"node": ">=6"
}
},
"node_modules/keyv": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
"integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
@@ -1361,6 +1594,14 @@
"node": ">=10"
}
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -1429,6 +1670,17 @@
"node": ">= 0.6"
}
},
"node_modules/mimic-response": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -1470,6 +1722,28 @@
"node": ">= 0.6"
}
},
"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/ntfy": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/ntfy/-/ntfy-1.0.5.tgz",
"integrity": "sha512-Epumie2o+GtyRbNVg//+gBIM2kcTsIGdPWtcHE1jG8ZKEgwkbXBnE84/TgdFib5O1WAzcn0D50NsOHfN+dQFkA==",
"dependencies": {
"axios": "1.3.1"
},
"engines": {
"node": ">= 10.9"
}
},
"node_modules/oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
@@ -1534,6 +1808,47 @@
"fn.name": "1.x.x"
}
},
"node_modules/p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"engines": {
"node": ">=8"
}
},
"node_modules/p-event": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz",
"integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==",
"dependencies": {
"p-timeout": "^3.1.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
"engines": {
"node": ">=4"
}
},
"node_modules/p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"dependencies": {
"p-finally": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1639,6 +1954,20 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@@ -1721,6 +2050,17 @@
"node": ">=0.10.0"
}
},
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"dependencies": {
"lowercase-keys": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -2058,6 +2398,14 @@
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"node_modules/to-readable-stream": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-2.1.0.tgz",
"integrity": "sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w==",
"engines": {
"node": ">=8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -2147,6 +2495,17 @@
"json5": "lib/cli.js"
}
},
"node_modules/type-fest": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.10.0.tgz",
"integrity": "sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw==",
"engines": {
"node": ">=8"
},
"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",
+2
View File
@@ -46,8 +46,10 @@
"express": "^4.17.1",
"express-session": "^1.17.2",
"formidable": "^2.1",
"gotify": "^1.1.0",
"json5": "^2.2.3",
"lastfm-node-client": "^2.2.0",
"ntfy": "^1.0.5",
"passport": "^0.6.0",
"passport-deezer": "^0.2.0",
"pony-cause": "^1.1.1",
+5 -1
View File
@@ -11,6 +11,7 @@ import {
import {Logger} from "winston";
import {CommonClientConfig} from "../common/infrastructure/config/client/index.js";
import {ClientConfig} from "../common/infrastructure/config/client/clients.js";
import {Notifiers} from "../notifier/Notifiers.js";
export default abstract class AbstractScrobbleClient {
@@ -37,11 +38,14 @@ export default abstract class AbstractScrobbleClient {
config: CommonClientConfig;
logger: Logger;
constructor(type: any, name: any, config: CommonClientConfig) {
notifier: Notifiers;
constructor(type: any, name: any, config: CommonClientConfig, notifier: Notifiers) {
this.type = type;
this.name = name;
const identifier = `Client ${capitalize(this.type)} - ${name}`;
this.logger = createLabelledLogger(identifier, identifier);
this.notifier = notifier;
const {
data: {
+6 -3
View File
@@ -2,7 +2,7 @@ import AbstractScrobbleClient from "./AbstractScrobbleClient.js";
import dayjs from 'dayjs';
import {
buildTrackString,
buildTrackString, capitalize,
playObjDataMatch, removeUndefinedKeys,
setIntersection, sleep,
sortByPlayDate,
@@ -12,6 +12,7 @@ import LastfmApiClient from "../apis/LastfmApiClient.js";
import {INITIALIZING, PlayObject, TrackStringOptions} from "../common/infrastructure/Atomic.js";
import {LastfmClientConfig} from "../common/infrastructure/config/client/lastfm.js";
import {TrackScrobbleResponse, UserGetRecentTracksResponse} from "lastfm-node-client";
import {Notifiers} from "../notifier/Notifiers.js";
export default class LastfmScrobbler extends AbstractScrobbleClient {
@@ -21,8 +22,8 @@ export default class LastfmScrobbler extends AbstractScrobbleClient {
declare config: LastfmClientConfig;
constructor(name: any, config: LastfmClientConfig, options = {}) {
super('lastfm', name, config);
constructor(name: any, config: LastfmClientConfig, options = {}, notifier: Notifiers) {
super('lastfm', name, config, notifier);
this.api = new LastfmApiClient(name, config.data, options)
}
@@ -312,12 +313,14 @@ export default class LastfmScrobbler extends AbstractScrobbleClient {
this.logger.info(`Scrobbled (Backlog) => (${source}) ${buildTrackString(playObj)}`);
}
if(ignored > 0) {
await this.notifier.notify({title: `${capitalize(this.type)} - ${this.name} Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: Service ignored this scrobble 😬 => (Code ${ignoreCode}) ${(ignoreMsg === '' ? '(No error message returned)' : ignoreMsg)}`, priority: 'warn'});
this.logger.warn(`Service ignored this scrobble 😬 => (Code ${ignoreCode}) ${(ignoreMsg === '' ? '(No error message returned)' : ignoreMsg)} -- See https://www.last.fm/api/errorcodes for more information`, {payload: scrobblePayload});
}
// last fm has rate limits but i can't find a specific example of what that limit is. going to default to 1 scrobble/sec to be safe
await sleep(1000);
} catch (e) {
await this.notifier.notify({title: `${capitalize(this.type)} - ${this.name} Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: ${e.message}`, priority: 'error'});
this.logger.error(`Scrobble Error (${sType})`, {playInfo: buildTrackString(playObj), payload: scrobblePayload});
throw e;
} finally {
+5 -3
View File
@@ -9,7 +9,7 @@ import {
sleep,
sortByPlayDate,
truncateStringToLength,
parseRetryAfterSecsFromObj
parseRetryAfterSecsFromObj, capitalize
} from "../utils.js";
import {
INITIALIZING,
@@ -23,6 +23,7 @@ import {
TrackStringOptions
} from "../common/infrastructure/Atomic.js";
import {MalojaClientConfig} from "../common/infrastructure/config/client/maloja.js";
import {Notifiers} from "../notifier/Notifiers.js";
const feat = ["ft.", "ft", "feat.", "feat", "featuring", "Ft.", "Ft", "Feat.", "Feat", "Featuring"];
@@ -34,8 +35,8 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
declare config: MalojaClientConfig
constructor(name: any, config: MalojaClientConfig) {
super('maloja', name, config);
constructor(name: any, config: MalojaClientConfig, notifier: Notifiers) {
super('maloja', name, config, notifier);
const {url, apiKey} = config.data;
if (apiKey === undefined) {
this.logger.warn("'apiKey' not found in config! Client will most likely fail when trying to scrobble");
@@ -572,6 +573,7 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
}
this.logger.debug('Payload:', scrobbleData);
} catch (e) {
await this.notifier.notify({title: `${capitalize(this.type)} - ${this.name} Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: ${e.message}`, priority: 'error'});
this.logger.error(`Scrobble Error (${sType})`, {playInfo: buildTrackString(playObj), payload: scrobbleData});
throw e;
}
+6 -5
View File
@@ -15,6 +15,7 @@ import * as clientSchema from '../common/schema/client.json';
import {ClientAIOConfig, ClientConfig} from "../common/infrastructure/config/client/clients.js";
import {MalojaClientConfig} from "../common/infrastructure/config/client/maloja.js";
import {LastfmClientConfig} from "../common/infrastructure/config/client/lastfm.js";
import {Notifiers} from "../notifier/Notifiers.js";
type groupedNamedConfigs = {[key: string]: ParsedConfig[]};
@@ -40,7 +41,7 @@ export default class ScrobbleClients {
return this.clients.filter(x => x.type === type);
}
buildClientsFromConfig = async () => {
buildClientsFromConfig = async (notifier: Notifiers) => {
let configs: ParsedConfig[] = [];
let configFile;
@@ -218,7 +219,7 @@ ${sources.join('\n')}`);
}));
for (const c of finalConfigs) {
try {
await this.addClient(c, clientDefaults);
await this.addClient(c, clientDefaults, notifier);
} catch(e) {
this.logger.error(`Client ${c.name} was not added because it had unrecoverable errors`);
this.logger.error(e);
@@ -226,7 +227,7 @@ ${sources.join('\n')}`);
}
}
addClient = async (clientConfig: ParsedConfig, defaults = {}) => {
addClient = async (clientConfig: ParsedConfig, defaults = {}, notifier: Notifiers) => {
/* const isValidConfig = isValidConfigStructure(clientConfig, {name: true, data: true, type: true});
if (isValidConfig !== true) {
throw new Error(`Config object from ${clientConfig.source || 'unknown'} with name [${clientConfig.name || 'unnamed'}] of type [${clientConfig.type || 'unknown'}] has errors: ${isValidConfig.join(' | ')}`)
@@ -238,10 +239,10 @@ ${sources.join('\n')}`);
this.logger.debug(`(${name}) Constructing ${type} client...`);
switch (type) {
case 'maloja':
newClient = new MalojaScrobbler(name, ({...clientConfig, data} as unknown as MalojaClientConfig));
newClient = new MalojaScrobbler(name, ({...clientConfig, data} as unknown as MalojaClientConfig), notifier);
break;
case 'lastfm':
newClient = new LastfmScrobbler(name, {...clientConfig, data: {configDir: this.configDir, ...data} } as unknown as LastfmClientConfig );
newClient = new LastfmScrobbler(name, {...clientConfig, data: {configDir: this.configDir, ...data} } as unknown as LastfmClientConfig, {}, notifier);
break;
default:
break;
@@ -2,12 +2,15 @@ import {SourceRetryOptions} from "./source/index.js";
import {RequestRetryOptions} from "./common.js";
import {SourceAIOConfig} from "./source/sources.js";
import {ClientAIOConfig} from "./client/clients.js";
import {WebhookConfig} from "./health/webhooks.js";
export interface AIOConfig {
sourceDefaults?: SourceRetryOptions
clientDefaults?: RequestRetryOptions
sources?: SourceAIOConfig[]
clients?: ClientAIOConfig[]
webhooks?: WebhookConfig[]
}
export interface AIOClientConfig {
@@ -0,0 +1,93 @@
export interface WebhookPayload {
title?: string
message: string
priority: 'info' | 'warn' | 'error'
}
export interface PrioritiesConfig {
/**
* @examples [5]
* */
info: number
/**
* @examples [7]
* */
warn: number
/**
* @examples [10]
* */
error: number
}
export interface CommonWebhookConfig {
/**
* Webhook type. Valid values are:
*
* * gotify
* * ntfy
*
* @examples ["gotify"]
* */
type: 'gotify' | 'ntfy'
/**
* A friendly name used to identify webhook config in logs
* */
name?: string
}
export interface GotifyConfig extends CommonWebhookConfig {
/**
* The URL of the Gotify server. Same URL that would be used to reach the Gotify UI
*
* @examples ["http://192.168.0.100:8078"]
* */
url: string
/**
* The token created for this Application in Gotify
*
* @examples ["AQZI58fA.rfSZbm"]
* */
token: string
/**
* Priority of messages
*
* * Info -> 5
* * Warn -> 7
* * Error -> 10
* */
priorities?: PrioritiesConfig
}
export interface NtfyConfig extends CommonWebhookConfig {
/**
* The URL of the Ntfy server
*
* @examples ["http://192.168.0.100:8078"]
* */
url: string
/**
* The topic mutli-scrobbler should POST to
* */
topic: string
/**
* Required if topic is protected
* */
username?: string
/**
* Required if topic is protected
* */
password?: string
/**
* Priority of messages
*
* * Info -> 3
* * Warn -> 4
* * Error -> 5
* */
priorities?: PrioritiesConfig
}
export type WebhookConfig = GotifyConfig | NtfyConfig;
+135
View File
@@ -164,6 +164,49 @@
],
"type": "object"
},
"GotifyConfig": {
"properties": {
"name": {
"description": "A friendly name used to identify webhook config in logs",
"type": "string"
},
"priorities": {
"$ref": "#/definitions/PrioritiesConfig",
"description": "Priority of messages\n\n* Info -> 5\n* Warn -> 7\n* Error -> 10"
},
"token": {
"description": "The token created for this Application in Gotify",
"examples": [
"AQZI58fA.rfSZbm"
],
"type": "string"
},
"type": {
"description": "Webhook type. Valid values are:\n\n* gotify\n* ntfy",
"enum": [
"gotify",
"ntfy"
],
"examples": [
"gotify"
],
"type": "string"
},
"url": {
"description": "The URL of the Gotify server. Same URL that would be used to reach the Gotify UI",
"examples": [
"http://192.168.0.100:8078"
],
"type": "string"
}
},
"required": [
"token",
"type",
"url"
],
"type": "object"
},
"JellyData": {
"properties": {
"maxPollRetries": {
@@ -612,6 +655,54 @@
},
"type": "object"
},
"NtfyConfig": {
"properties": {
"name": {
"description": "A friendly name used to identify webhook config in logs",
"type": "string"
},
"password": {
"description": "Required if topic is protected",
"type": "string"
},
"priorities": {
"$ref": "#/definitions/PrioritiesConfig",
"description": "Priority of messages\n\n* Info -> 3\n* Warn -> 4\n* Error -> 5"
},
"topic": {
"description": "The topic mutli-scrobbler should POST to",
"type": "string"
},
"type": {
"description": "Webhook type. Valid values are:\n\n* gotify\n* ntfy",
"enum": [
"gotify",
"ntfy"
],
"examples": [
"gotify"
],
"type": "string"
},
"url": {
"description": "The URL of the Ntfy server",
"examples": [
"http://192.168.0.100:8078"
],
"type": "string"
},
"username": {
"description": "Required if topic is protected",
"type": "string"
}
},
"required": [
"topic",
"type",
"url"
],
"type": "object"
},
"PlexSourceAIOConfig": {
"properties": {
"clients": {
@@ -738,6 +829,34 @@
},
"type": "object"
},
"PrioritiesConfig": {
"properties": {
"error": {
"examples": [
10
],
"type": "number"
},
"info": {
"examples": [
5
],
"type": "number"
},
"warn": {
"examples": [
7
],
"type": "number"
}
},
"required": [
"error",
"info",
"warn"
],
"type": "object"
},
"Record<string,any>": {
"type": "object"
},
@@ -1046,6 +1165,16 @@
],
"type": "object"
},
"WebhookConfig": {
"anyOf": [
{
"$ref": "#/definitions/GotifyConfig"
},
{
"$ref": "#/definitions/NtfyConfig"
}
]
},
"YTMusicData": {
"properties": {
"authUser": {
@@ -1146,6 +1275,12 @@
"$ref": "#/definitions/SourceAIOConfig"
},
"type": "array"
},
"webhooks": {
"items": {
"$ref": "#/definitions/WebhookConfig"
},
"type": "array"
}
},
"type": "object"
+11 -2
View File
@@ -39,6 +39,8 @@ import SpotifySource from "./sources/SpotifySource.js";
import {JellyfinNotifier} from "./sources/ingressNotifiers/JellyfinNotifier.js";
import {PlexNotifier} from "./sources/ingressNotifiers/PlexNotifier.js";
import {TautulliNotifier} from "./sources/ingressNotifiers/TautulliNotifier.js";
import {Notifiers} from "./notifier/Notifiers.js";
import {AIOConfig} from "./common/infrastructure/config/aioConfig.js";
dayjs.extend(utc)
@@ -138,17 +140,24 @@ const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`)
logger.warn('App config file exists but could not be parsed!');
}
const notifiers = new Notifiers();
const {
webhooks = []
} = (config || {}) as AIOConfig;
await notifiers.buildWebhooks(webhooks)
/*
* setup clients
* */
const scrobbleClients = new ScrobbleClients(configDir);
await scrobbleClients.buildClientsFromConfig();
await scrobbleClients.buildClientsFromConfig(notifiers);
if (scrobbleClients.clients.length === 0) {
logger.warn('No scrobble clients were configured!')
}
const scrobbleSources = new ScrobbleSources(localUrl, configDir);
await scrobbleSources.buildSourcesFromConfig();
await scrobbleSources.buildSourcesFromConfig([], notifiers);
const clientCheckMiddle = makeClientCheckMiddle(scrobbleClients);
const sourceCheckMiddle = makeSourceCheckMiddle(scrobbleSources);
+24
View File
@@ -0,0 +1,24 @@
import {GotifyConfig, NtfyConfig, WebhookPayload} from "../common/infrastructure/config/health/webhooks.js";
import {Logger} from "winston";
import {createLabelledLogger} from "../utils.js";
export abstract class AbstractWebhookNotifier {
config: GotifyConfig | NtfyConfig
logger: Logger;
initialized: boolean = false;
protected constructor(type: string, defaultName: string, config: GotifyConfig | NtfyConfig) {
this.config = config;
const label = `${type} - ${config.name ?? defaultName}`
this.logger = createLabelledLogger(label, label);
}
initialize = async () => {
this.initialized = true;
this.logger.verbose('Initialized');
}
abstract notify: (payload: WebhookPayload) => Promise<any>;
}
+42
View File
@@ -0,0 +1,42 @@
import {AbstractWebhookNotifier} from "./AbstractWebhookNotifier.js";
import {GotifyConfig, PrioritiesConfig, WebhookPayload} from "../common/infrastructure/config/health/webhooks.js";
import gotify from 'gotify';
export class GotifyWebhookNotifier extends AbstractWebhookNotifier {
declare config: GotifyConfig;
priorities: PrioritiesConfig;
constructor(defaultName: string, config: GotifyConfig) {
super('Gotify', defaultName, config);
const {
info = 5,
warn = 7,
error = 10
} = this.config.priorities || {};
this.priorities = {
info,
warn,
error
}
}
notify = async (payload: WebhookPayload) => {
try {
await gotify.gotify({
server: this.config.url,
app: this.config.token,
message: payload.message,
title: payload.title,
priority: this.priorities[payload.priority]
});
this.logger.debug(`Pushed notification.`);
} catch (e: any) {
this.logger.error(`Failed to push notification: ${e.message}`)
}
}
}
+48
View File
@@ -0,0 +1,48 @@
import {config, Logger} from "winston";
import {createLabelledLogger} from "../utils.js";
import {
GotifyConfig,
NtfyConfig,
WebhookConfig,
WebhookPayload
} from "../common/infrastructure/config/health/webhooks.js";
import {AbstractWebhookNotifier} from "./AbstractWebhookNotifier.js";
import {GotifyWebhookNotifier} from "./GotifyWebhookNotifier.js";
import {NtfyWebhookNotifier} from "./NtfyWebhookNotifier.js";
export class Notifiers {
logger: Logger;
webhooks: AbstractWebhookNotifier[];
constructor() {
this.logger = createLabelledLogger('Notifiers', 'Notifiers');
}
buildWebhooks = async (webhookConfigs: WebhookConfig[]) => {
for (const [i, config] of Object.entries(webhookConfigs)) {
let webhook: AbstractWebhookNotifier;
const defaultName = `Config ${i}`
switch (config.type) {
case 'gotify':
webhook = new GotifyWebhookNotifier(defaultName, config as GotifyConfig);
break;
case 'ntfy':
webhook = new NtfyWebhookNotifier(defaultName, config as NtfyConfig);
break;
default:
this.logger.error(`'${config.type}' is not a valid webhook type`);
continue;
}
await webhook.initialize();
this.webhooks.push(webhook);
}
}
notify = async (payload: WebhookPayload) => {
for (const webhook of this.webhooks) {
await webhook.notify(payload);
}
}
}
+54
View File
@@ -0,0 +1,54 @@
import {AbstractWebhookNotifier} from "./AbstractWebhookNotifier.js";
import {
NtfyConfig,
PrioritiesConfig,
WebhookPayload
} from "../common/infrastructure/config/health/webhooks.js";
import {publish} from 'ntfy';
export class NtfyWebhookNotifier extends AbstractWebhookNotifier {
declare config: NtfyConfig;
priorities: PrioritiesConfig;
constructor(defaultName: string, config: NtfyConfig) {
super('Ntfy', defaultName, config);
const {
info = 3,
warn = 4,
error = 5
} = this.config.priorities || {};
this.priorities = {
info,
warn,
error
}
}
notify = async (payload: WebhookPayload) => {
try {
let authorization = {};
if (this.config.username !== undefined) {
authorization = {
username: this.config.username,
password: this.config.password,
}
}
await publish({
message: payload.message,
topic: this.config.topic,
title: payload.title,
server: this.config.url,
priority: this.priorities[payload.priority],
...authorization,
});
this.logger.debug(`Pushed notification.`);
} catch (e: any) {
this.logger.error(`Failed to push notification: ${e.message}`)
}
}
}
+9 -2
View File
@@ -3,6 +3,7 @@ import {buildTrackString, capitalize, createLabelledLogger, sleep} from "../util
import {InternalConfig, PlayObject, SourceType} from "../common/infrastructure/Atomic.js";
import {Logger} from "winston";
import {SourceConfig} from "../common/infrastructure/config/source/sources.js";
import {Notifiers} from "../notifier/Notifiers.js";
export interface RecentlyPlayedOptions {
limit?: number
@@ -20,6 +21,7 @@ export default abstract class AbstractSource {
config: SourceConfig;
clients: string[];
logger: Logger;
notifier: Notifiers;
instantiatedAt: Dayjs;
initialized: boolean = false;
requiresAuth: boolean = false;
@@ -35,12 +37,13 @@ export default abstract class AbstractSource {
pollRetries: number = 0;
tracksDiscovered: number = 0;
constructor(type: SourceType, name: string, config: SourceConfig, internal: InternalConfig) {
constructor(type: SourceType, name: string, config: SourceConfig, internal: InternalConfig, notifiers: Notifiers) {
const {clients = [] } = config;
this.type = type;
this.name = name;
this.identifier = `Source - ${capitalize(this.type)} - ${name}`;
this.logger = createLabelledLogger(this.identifier, this.identifier);
this.notifier = notifiers;
this.config = config;
this.clients = clients;
this.instantiatedAt = dayjs();
@@ -76,6 +79,7 @@ export default abstract class AbstractSource {
startPolling = async (allClients: any) => {
if(this.requiresAuthInteraction && !this.authed) {
await this.notifier.notify({title: `${this.identifier} Polling`, message: 'Cannot start polling because user interaction is required for authentication', priority: 'error'});
this.logger.error('Cannot start polling because user interaction is required for authentication');
return;
}
@@ -98,10 +102,12 @@ export default abstract class AbstractSource {
} catch (e) {
if (this.pollRetries < maxRetries) {
const delayFor = (this.pollRetries + 1) * retryMultiplier;
this.logger.info(`Poll reties (${this.pollRetries}) less than max poll retries (${maxRetries}), restarting polling after ${delayFor} second delay...`);
this.logger.info(`Poll retries (${this.pollRetries}) less than max poll retries (${maxRetries}), restarting polling after ${delayFor} second delay...`);
await this.notifier.notify({title: `${this.identifier} Polling`, message: `Encountered error while polling but retries (${this.pollRetries}) are less than max poll retries (${maxRetries}), restarting polling after ${delayFor} second delay. | Error: ${e.message}`, priority: 'warn'});
await sleep((delayFor) * 1000);
} else {
this.logger.warn(`Poll retries (${this.pollRetries}) equal to max poll retries (${maxRetries}), stopping polling!`);
await this.notifier.notify({title: `${this.identifier} Polling`, message: `Encountered error while polling and retries (${this.pollRetries}) are equal to max poll retries (${maxRetries}), stopping polling!. | Error: ${e.message}`, priority: 'error'});
}
this.pollRetries++;
}
@@ -116,6 +122,7 @@ export default abstract class AbstractSource {
return;
}
this.logger.info('Polling started');
this.notifier.notify({title: `${this.identifier} Polling`, message: 'Polling Started', priority: 'info'});
let lastTrackPlayedAt = this.instantiatedAt;
let checkCount = 0;
let checksOverThreshold = 0;
+3 -2
View File
@@ -5,6 +5,7 @@ import AbstractSource, {RecentlyPlayedOptions} from "./AbstractSource.js";
import dayjs from "dayjs";
import {DeezerSourceConfig} from "../common/infrastructure/config/source/deezer.js";
import {InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import {Notifiers} from "../notifier/Notifiers.js";
export default class DeezerSource extends AbstractSource {
workingCredsPath;
@@ -17,8 +18,8 @@ export default class DeezerSource extends AbstractSource {
declare config: DeezerSourceConfig;
constructor(name: any, config: DeezerSourceConfig, internal: InternalConfig) {
super('deezer', name, config, internal);
constructor(name: any, config: DeezerSourceConfig, internal: InternalConfig, notifier: Notifiers) {
super('deezer', name, config, internal, notifier);
const {
data: {
interval = 60,
+3 -2
View File
@@ -3,6 +3,7 @@ import dayjs from "dayjs";
import {buildTrackString, parseDurationFromTimestamp} from "../utils.js";
import {JellySourceConfig} from "../common/infrastructure/config/source/jellyfin.js";
import {InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import {Notifiers} from "../notifier/Notifiers.js";
export default class JellyfinSource extends MemorySource {
@@ -11,8 +12,8 @@ export default class JellyfinSource extends MemorySource {
declare config: JellySourceConfig;
constructor(name: any, config: JellySourceConfig, internal: InternalConfig) {
super('jellyfin', name, config, internal);
constructor(name: any, config: JellySourceConfig, internal: InternalConfig, notifier: Notifiers) {
super('jellyfin', name, config, internal, notifier);
const {data: {users, servers} = {}} = config;
if (users === undefined || users === null) {
+3 -2
View File
@@ -4,6 +4,7 @@ import {sortByPlayDate} from "../utils.js";
import {LastfmClientConfig} from "../common/infrastructure/config/client/lastfm.js";
import {InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import {UserGetRecentTracksResponse} from "lastfm-node-client";
import {Notifiers} from "../notifier/Notifiers.js";
export default class LastfmSource extends AbstractSource {
@@ -13,8 +14,8 @@ export default class LastfmSource extends AbstractSource {
declare config: LastfmClientConfig;
constructor(name: any, config: LastfmClientConfig, internal: InternalConfig) {
super('lastfm', name, config, internal);
constructor(name: any, config: LastfmClientConfig, internal: InternalConfig, notifier: Notifiers) {
super('lastfm', name, config, internal, notifier);
this.canPoll = true;
this.api = new LastfmApiClient(name, config.data);
}
+3 -2
View File
@@ -5,6 +5,7 @@ import formidable from 'formidable';
import concatStream from 'concat-stream';
import {PlexSourceConfig} from "../common/infrastructure/config/source/plex.js";
import {InternalConfig, PlayObject, SourceType} from "../common/infrastructure/Atomic.js";
import {Notifiers} from "../notifier/Notifiers.js";
export default class PlexSource extends AbstractSource {
users: string[];
@@ -13,8 +14,8 @@ export default class PlexSource extends AbstractSource {
declare config: PlexSourceConfig;
constructor(name: any, config: PlexSourceConfig, internal: InternalConfig, type: SourceType = 'plex') {
super(type, name, config, internal);
constructor(name: any, config: PlexSourceConfig, internal: InternalConfig, type: SourceType = 'plex', notifier: Notifiers) {
super(type, name, config, internal, notifier);
const {data: {user = [], libraries = [], servers = []} = {}} = config
if (!Array.isArray(user)) {
+12 -11
View File
@@ -24,6 +24,7 @@ import * as sourceSchema from "../common/schema/source.json";
import {LastfmSourceConfig} from "../common/infrastructure/config/source/lastfm.js";
import YTMusicSource from "./YTMusicSource.js";
import {YTMusicSourceConfig} from "../common/infrastructure/config/source/ytmusic.js";
import {Notifiers} from "../notifier/Notifiers.js";
type groupedNamedConfigs = {[key: string]: ParsedConfig[]};
@@ -54,7 +55,7 @@ export default class ScrobbleSources {
return this.sources.find(x => x.name === name && x.type === type);
}
buildSourcesFromConfig = async (additionalConfigs: ParsedConfig[] = []) => {
buildSourcesFromConfig = async (additionalConfigs: ParsedConfig[] = [], notifier: Notifiers) => {
let configs: ParsedConfig[] = additionalConfigs;
let configFile;
@@ -289,7 +290,7 @@ export default class ScrobbleSources {
// @ts-expect-error TS(2571): Object is of type 'unknown'.
for (const c of tempNamedConfigs) {
try {
await this.addSource(c, sourceDefaults);
await this.addSource(c, sourceDefaults, notifier);
} catch(e) {
this.logger.error(`Source ${c.name} of type ${c.type} was not added because of unrecoverable errors`);
this.logger.error(e);
@@ -299,7 +300,7 @@ export default class ScrobbleSources {
}
}
addSource = async (clientConfig: any, defaults = {}) => {
addSource = async (clientConfig: any, defaults = {}, notifier: Notifiers) => {
// const isValidConfig = isValidConfigStructure(clientConfig, {name: true, data: true, type: true});
// if (isValidConfig !== true) {
// throw new Error(`Config object from ${clientConfig.source || 'unknown'} with name [${clientConfig.name || 'unnamed'}] of type [${clientConfig.type || 'unknown'}] has errors: ${isValidConfig.join(' | ')}`)
@@ -320,28 +321,28 @@ export default class ScrobbleSources {
let newSource;
switch (type) {
case 'spotify':
newSource = new SpotifySource(name, compositeConfig as SpotifySourceConfig, internal);
newSource = new SpotifySource(name, compositeConfig as SpotifySourceConfig, internal, notifier);
break;
case 'plex':
newSource = await new PlexSource(name, compositeConfig as PlexSourceConfig, internal);
newSource = await new PlexSource(name, compositeConfig as PlexSourceConfig, internal, 'plex', notifier);
break;
case 'tautulli':
newSource = await new TautulliSource(name, compositeConfig as TautulliSourceConfig, internal);
newSource = await new TautulliSource(name, compositeConfig as TautulliSourceConfig, internal, notifier);
break;
case 'subsonic':
newSource = new SubsonicSource(name, compositeConfig as SubSonicSourceConfig, internal);
newSource = new SubsonicSource(name, compositeConfig as SubSonicSourceConfig, internal, notifier);
break;
case 'jellyfin':
newSource = await new JellyfinSource(name, compositeConfig as JellySourceConfig, internal);
newSource = await new JellyfinSource(name, compositeConfig as JellySourceConfig, internal, notifier);
break;
case 'lastfm':
newSource = await new LastfmSource(name, compositeConfig as LastfmClientConfig, internal);
newSource = await new LastfmSource(name, compositeConfig as LastfmClientConfig, internal, notifier);
break;
case 'deezer':
newSource = await new DeezerSource(name, compositeConfig as DeezerSourceConfig, internal);
newSource = await new DeezerSource(name, compositeConfig as DeezerSourceConfig, internal, notifier);
break;
case 'ytmusic':
newSource = await new YTMusicSource(name, compositeConfig as YTMusicSourceConfig, internal);
newSource = await new YTMusicSource(name, compositeConfig as YTMusicSourceConfig, internal, notifier);
default:
break;
}
+3 -2
View File
@@ -9,6 +9,7 @@ import AbstractSource, {RecentlyPlayedOptions} from "./AbstractSource.js";
import {SpotifySourceConfig} from "../common/infrastructure/config/source/spotify.js";
import {InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import PlayHistoryObject = SpotifyApi.PlayHistoryObject;
import {Notifiers} from "../notifier/Notifiers.js";
const scopes = ['user-read-recently-played', 'user-read-currently-playing'];
const state = 'random';
@@ -23,8 +24,8 @@ export default class SpotifySource extends AbstractSource {
declare config: SpotifySourceConfig;
constructor(name: any, config: SpotifySourceConfig, internal: InternalConfig) {
super('spotify', name, config, internal);
constructor(name: any, config: SpotifySourceConfig, internal: InternalConfig, notifier: Notifiers) {
super('spotify', name, config, internal, notifier);
const {
data: {
interval = 60,
+3 -2
View File
@@ -7,6 +7,7 @@ import MemorySource from "./MemorySource.js";
import {SubSonicSourceConfig} from "../common/infrastructure/config/source/subsonic.js";
import {InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import {RecentlyPlayedOptions} from "./AbstractSource.js";
import {Notifiers} from "../notifier/Notifiers.js";
dayjs.extend(isSameOrAfter);
@@ -16,7 +17,7 @@ export class SubsonicSource extends MemorySource {
declare config: SubSonicSourceConfig;
constructor(name: any, config: SubSonicSourceConfig, internal: InternalConfig) {
constructor(name: any, config: SubSonicSourceConfig, internal: InternalConfig, notifier: Notifiers) {
// default to quick interval so we can get a decently accurate nowPlaying
const {
data: {
@@ -28,7 +29,7 @@ export class SubsonicSource extends MemorySource {
} = {}
} = config;
const subsonicConfig = {...config, data: {...restData, internal, maxSleep}};
super('subsonic', name, subsonicConfig, internal);
super('subsonic', name, subsonicConfig, internal, notifier);
const {data: {user, password, url} = {}} = this.config;
+3 -2
View File
@@ -2,12 +2,13 @@ import dayjs from "dayjs";
import PlexSource from "./PlexSource.js";
import {TautulliSourceConfig} from "../common/infrastructure/config/source/tautulli.js";
import {InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import {Notifiers} from "../notifier/Notifiers.js";
export default class TautulliSource extends PlexSource {
declare config: TautulliSourceConfig;
constructor(name: any, config: TautulliSourceConfig, internal: InternalConfig) {
super(name, config, internal, 'tautulli');
constructor(name: any, config: TautulliSourceConfig, internal: InternalConfig, notifier: Notifiers) {
super(name, config, internal, 'tautulli', notifier);
}
static formatPlayObj(obj: any, newFromSource = false): PlayObject {
+3 -2
View File
@@ -7,6 +7,7 @@ import dayjs from "dayjs";
import {parseDurationFromTimestamp, playObjDataMatch} from "../utils.js";
import {ITrackDetail} from "youtube-music-ts-api/interfaces-supplementary";
import {YTMusicSourceConfig} from "../common/infrastructure/config/source/ytmusic.js";
import {Notifiers} from "../notifier/Notifiers.js";
export default class YTMusicSource extends AbstractSource {
apiInstance?: IYouTubeMusicAuthenticated
@@ -17,8 +18,8 @@ export default class YTMusicSource extends AbstractSource {
recentlyPlayed: PlayObject[] = [];
constructor(name: string, config: YTMusicSourceConfig, internal: InternalConfig) {
super('ytmusic', name, config, internal);
constructor(name: string, config: YTMusicSourceConfig, internal: InternalConfig, notifier: Notifiers) {
super('ytmusic', name, config, internal, notifier);
this.canPoll = true;
}