mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
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:
Generated
+359
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user