Repository: web-scrobbler/web-scrobbler
Pull request #3908 (2023-06-20 20:45:56)

Added Pleroma scrobbling
**Describe the changes you made**
Added Pleroma scrobbling

**Additional context**
PleromaBE is a microblogging service that is compatible with Mastodon. Its API has scrobbling support. This PR added that.

TheStranjer: This is very similar to the Maloja scrobbler with a few differences. That being said, it does work.

It working in-browser:
![image](https://github.com/web-scrobbler/web-scrobbler/assets/791672/b10033f1-82d7-4532-9ff7-209f148c60c9)

Pleroma `GET` API working:
![image](https://github.com/web-scrobbler/web-scrobbler/assets/791672/84f9caff-a3d8-4e44-8f57-1391deefe8ac)

I will need to implement something for the frontend to view these results but having a web scrobbler support Pleroma will bring things forward quite a bit.

inverse: @yayuyokitano  what do you think of this change?  Seems reasonable to me
TheStranjer: @yayuyokitano I just fixed the merge conflicts. Merry Christmas! 🎅 

Patch:
diff --git a/README.md b/README.md
index 4141608c..28847f22 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ Web Scrobbler helps online music listeners to scrobble their playback history.
 -   [Libre.fm][LibreFm]
 -   [ListenBrainz][ListenBrainz]
 -   [Maloja][Maloja]
+-   [Pleroma][Pleroma]
 
 ## Installation
 
@@ -147,6 +148,7 @@ Licensed under the [MIT License][License].
 [LibreFm]: https://libre.fm/
 [ListenBrainz]: https://listenbrainz.org/
 [Maloja]: https://github.com/krateng/maloja
+[Pleroma]: https://pleroma.social/
 
 <!-- Wiki pages -->
 
diff --git a/package-lock.json b/package-lock.json
index 9d2fed51..b60077bc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,16 @@
 {
   "name": "web-scrobbler",
-  "version": "3.3.0",
+  "version": "3.4.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "web-scrobbler",
-      "version": "3.3.0",
+      "version": "3.4.0",
       "license": "MIT",
       "dependencies": {
         "@suid/icons-material": "0.6.11",
-        "@web-scrobbler/metadata-filter": "3.0.2",
+        "@web-scrobbler/metadata-filter": "3.1.0",
         "blueimp-md5": "2.19.0",
         "stylelint": "15.11.0",
         "vite": "4.5.1",
@@ -22,37 +22,37 @@
         "@types/fs-extra": "11.0.3",
         "@types/imagemin": "8.0.5",
         "@types/imagemin-jpegtran": "5.0.4",
-        "@types/node": "20.10.4",
-        "@types/react": "18.2.43",
-        "@types/webextension-polyfill": "0.10.4",
-        "@typescript-eslint/eslint-plugin": "6.13.2",
+        "@types/node": "20.10.6",
+        "@types/react": "18.2.47",
+        "@types/webextension-polyfill": "0.10.7",
+        "@typescript-eslint/eslint-plugin": "6.18.0",
         "@web-scrobbler/eslint-config": "3.0.0",
         "@web-scrobbler/stylelint-config": "1.0.1",
         "archiver": "6.0.1",
         "canvas": "2.11.2",
         "chokidar": "3.5.3",
-        "esbuild": "0.19.5",
-        "eslint": "8.55.0",
+        "esbuild": "0.19.11",
+        "eslint": "8.56.0",
         "eslint-plugin-react": "7.33.2",
-        "eslint-plugin-solid": "0.13.0",
+        "eslint-plugin-solid": "0.13.1",
         "eslint-plugin-tsdoc": "0.2.17",
         "fs-extra": "11.1.1",
         "glob": "10.3.10",
         "imagemin": "8.0.1",
         "imagemin-jpegtran": "7.0.0",
         "imagemin-pngquant": "9.0.2",
-        "prettier": "3.0.3",
+        "prettier": "3.1.1",
         "prettier-config-web-scrobbler": "0.1.0",
         "remark": "15.0.1",
         "remark-cli": "12.0.0",
         "remark-preset-lint-recommended": "6.1.3",
-        "sass": "1.69.5",
-        "solid-js": "1.8.5",
-        "tsx": "4.6.2",
+        "sass": "1.69.7",
+        "solid-js": "1.8.8",
+        "tsx": "4.7.0",
         "typescript": "5.3.3",
         "vite-plugin-conditional-compiler": "0.2.1",
         "vite-plugin-solid": "2.8.0",
-        "vite-plugin-static-copy": "0.17.0",
+        "vite-plugin-static-copy": "0.17.1",
         "vitest": "0.34.6"
       },
       "engines": {
@@ -836,10 +836,26 @@
         "node": ">=16"
       }
     },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
+      "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.5.tgz",
-      "integrity": "sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz",
+      "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==",
       "cpu": [
         "arm"
       ],
@@ -853,9 +869,9 @@
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz",
-      "integrity": "sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz",
+      "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==",
       "cpu": [
         "arm64"
       ],
@@ -869,9 +885,9 @@
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.5.tgz",
-      "integrity": "sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz",
+      "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==",
       "cpu": [
         "x64"
       ],
@@ -885,9 +901,9 @@
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz",
-      "integrity": "sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz",
+      "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==",
       "cpu": [
         "arm64"
       ],
@@ -901,9 +917,9 @@
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz",
-      "integrity": "sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz",
+      "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==",
       "cpu": [
         "x64"
       ],
@@ -917,9 +933,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz",
-      "integrity": "sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz",
+      "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==",
       "cpu": [
         "arm64"
       ],
@@ -933,9 +949,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz",
-      "integrity": "sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz",
+      "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==",
       "cpu": [
         "x64"
       ],
@@ -949,9 +965,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz",
-      "integrity": "sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz",
+      "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==",
       "cpu": [
         "arm"
       ],
@@ -965,9 +981,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz",
-      "integrity": "sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz",
+      "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==",
       "cpu": [
         "arm64"
       ],
@@ -981,9 +997,9 @@
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz",
-      "integrity": "sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz",
+      "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==",
       "cpu": [
         "ia32"
       ],
@@ -997,9 +1013,9 @@
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz",
-      "integrity": "sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz",
+      "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==",
       "cpu": [
         "loong64"
       ],
@@ -1013,9 +1029,9 @@
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz",
-      "integrity": "sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz",
+      "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==",
       "cpu": [
         "mips64el"
       ],
@@ -1029,9 +1045,9 @@
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz",
-      "integrity": "sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz",
+      "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==",
       "cpu": [
         "ppc64"
       ],
@@ -1045,9 +1061,9 @@
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz",
-      "integrity": "sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz",
+      "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==",
       "cpu": [
         "riscv64"
       ],
@@ -1061,9 +1077,9 @@
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz",
-      "integrity": "sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz",
+      "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==",
       "cpu": [
         "s390x"
       ],
@@ -1077,9 +1093,9 @@
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz",
-      "integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz",
+      "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==",
       "cpu": [
         "x64"
       ],
@@ -1093,9 +1109,9 @@
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz",
-      "integrity": "sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz",
+      "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==",
       "cpu": [
         "x64"
       ],
@@ -1109,9 +1125,9 @@
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz",
-      "integrity": "sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz",
+      "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==",
       "cpu": [
         "x64"
       ],
@@ -1125,9 +1141,9 @@
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz",
-      "integrity": "sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz",
+      "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==",
       "cpu": [
         "x64"
       ],
@@ -1141,9 +1157,9 @@
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz",
-      "integrity": "sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz",
+      "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==",
       "cpu": [
         "arm64"
       ],
@@ -1157,9 +1173,9 @@
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz",
-      "integrity": "sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz",
+      "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==",
       "cpu": [
         "ia32"
       ],
@@ -1173,9 +1189,9 @@
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz",
-      "integrity": "sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz",
+      "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==",
       "cpu": [
         "x64"
       ],
@@ -1236,9 +1252,9 @@
       }
     },
     "node_modules/@eslint/js": {
-      "version": "8.55.0",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
-      "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
+      "version": "8.56.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
+      "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1882,9 +1898,9 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "20.10.4",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz",
-      "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==",
+      "version": "20.10.6",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz",
+      "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==",
       "devOptional": true,
       "dependencies": {
         "undici-types": "~5.26.4"
@@ -1902,9 +1918,9 @@
       "dev": true
     },
     "node_modules/@types/react": {
-      "version": "18.2.43",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.43.tgz",
-      "integrity": "sha512-nvOV01ZdBdd/KW6FahSbcNplt2jCJfyWdTos61RYHV+FVv5L/g9AOX1bmbVcWcLFL8+KHQfh1zVIQrud6ihyQA==",
+      "version": "18.2.47",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz",
+      "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==",
       "dev": true,
       "dependencies": {
         "@types/prop-types": "*",
@@ -1952,22 +1968,22 @@
       "dev": true
     },
     "node_modules/@types/webextension-polyfill": {
-      "version": "0.10.4",
-      "resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.10.4.tgz",
-      "integrity": "sha512-pvEIqAZEbJRzaqTaWq3xlUoMWa3+euZHHz+VZHCzHWW+jOf8qLOq9wXy38U+WiPG3108SJC/wNc1X6vPC5TcjQ==",
+      "version": "0.10.7",
+      "resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.10.7.tgz",
+      "integrity": "sha512-10ql7A0qzBmFB+F+qAke/nP1PIonS0TXZAOMVOxEUsm+lGSW6uwVcISFNa0I4Oyj0884TZVWGGMIWeXOVSNFHw==",
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz",
-      "integrity": "sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.18.0.tgz",
+      "integrity": "sha512-3lqEvQUdCozi6d1mddWqd+kf8KxmGq2Plzx36BlkjuQe3rSTm/O98cLf0A4uDO+a5N1KD2SeEEl6fW97YHY+6w==",
       "dev": true,
       "dependencies": {
         "@eslint-community/regexpp": "^4.5.1",
-        "@typescript-eslint/scope-manager": "6.13.2",
-        "@typescript-eslint/type-utils": "6.13.2",
-        "@typescript-eslint/utils": "6.13.2",
-        "@typescript-eslint/visitor-keys": "6.13.2",
+        "@typescript-eslint/scope-manager": "6.18.0",
+        "@typescript-eslint/type-utils": "6.18.0",
+        "@typescript-eslint/utils": "6.18.0",
+        "@typescript-eslint/visitor-keys": "6.18.0",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "ignore": "^5.2.4",
@@ -1993,13 +2009,13 @@
       }
     },
     "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz",
-      "integrity": "sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.0.tgz",
+      "integrity": "sha512-o/UoDT2NgOJ2VfHpfr+KBY2ErWvCySNUIX/X7O9g8Zzt/tXdpfEU43qbNk8LVuWUT2E0ptzTWXh79i74PP0twA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.13.2",
-        "@typescript-eslint/visitor-keys": "6.13.2"
+        "@typescript-eslint/types": "6.18.0",
+        "@typescript-eslint/visitor-keys": "6.18.0"
       },
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2010,9 +2026,9 @@
       }
     },
     "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz",
-      "integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.0.tgz",
+      "integrity": "sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==",
       "dev": true,
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2023,12 +2039,12 @@
       }
     },
     "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz",
-      "integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz",
+      "integrity": "sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.13.2",
+        "@typescript-eslint/types": "6.18.0",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
@@ -2087,13 +2103,13 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz",
-      "integrity": "sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.18.0.tgz",
+      "integrity": "sha512-ZeMtrXnGmTcHciJN1+u2CigWEEXgy1ufoxtWcHORt5kGvpjjIlK9MUhzHm4RM8iVy6dqSaZA/6PVkX6+r+ChjQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "6.13.2",
-        "@typescript-eslint/utils": "6.13.2",
+        "@typescript-eslint/typescript-estree": "6.18.0",
+        "@typescript-eslint/utils": "6.18.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.0.1"
       },
@@ -2114,9 +2130,9 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz",
-      "integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.0.tgz",
+      "integrity": "sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==",
       "dev": true,
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2127,16 +2143,17 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz",
-      "integrity": "sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.0.tgz",
+      "integrity": "sha512-klNvl+Ql4NsBNGB4W9TZ2Od03lm7aGvTbs0wYaFYsplVPhr+oeXjlPZCDI4U9jgJIDK38W1FKhacCFzCC+nbIg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.13.2",
-        "@typescript-eslint/visitor-keys": "6.13.2",
+        "@typescript-eslint/types": "6.18.0",
+        "@typescript-eslint/visitor-keys": "6.18.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
+        "minimatch": "9.0.3",
         "semver": "^7.5.4",
         "ts-api-utils": "^1.0.1"
       },
@@ -2154,12 +2171,12 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz",
-      "integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz",
+      "integrity": "sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.13.2",
+        "@typescript-eslint/types": "6.18.0",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
@@ -2170,6 +2187,30 @@
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
+    "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/@typescript-eslint/types": {
       "version": "6.7.3",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.3.tgz",
@@ -2213,17 +2254,17 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.2.tgz",
-      "integrity": "sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.18.0.tgz",
+      "integrity": "sha512-wiKKCbUeDPGaYEYQh1S580dGxJ/V9HI7K5sbGAVklyf+o5g3O+adnS4UNJajplF4e7z2q0uVBaTdT/yLb4XAVA==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@types/json-schema": "^7.0.12",
         "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "6.13.2",
-        "@typescript-eslint/types": "6.13.2",
-        "@typescript-eslint/typescript-estree": "6.13.2",
+        "@typescript-eslint/scope-manager": "6.18.0",
+        "@typescript-eslint/types": "6.18.0",
+        "@typescript-eslint/typescript-estree": "6.18.0",
         "semver": "^7.5.4"
       },
       "engines": {
@@ -2238,13 +2279,13 @@
       }
     },
     "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz",
-      "integrity": "sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.0.tgz",
+      "integrity": "sha512-o/UoDT2NgOJ2VfHpfr+KBY2ErWvCySNUIX/X7O9g8Zzt/tXdpfEU43qbNk8LVuWUT2E0ptzTWXh79i74PP0twA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.13.2",
-        "@typescript-eslint/visitor-keys": "6.13.2"
+        "@typescript-eslint/types": "6.18.0",
+        "@typescript-eslint/visitor-keys": "6.18.0"
       },
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2255,9 +2296,9 @@
       }
     },
     "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz",
-      "integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.0.tgz",
+      "integrity": "sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==",
       "dev": true,
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2268,16 +2309,17 @@
       }
     },
     "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz",
-      "integrity": "sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.0.tgz",
+      "integrity": "sha512-klNvl+Ql4NsBNGB4W9TZ2Od03lm7aGvTbs0wYaFYsplVPhr+oeXjlPZCDI4U9jgJIDK38W1FKhacCFzCC+nbIg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.13.2",
-        "@typescript-eslint/visitor-keys": "6.13.2",
+        "@typescript-eslint/types": "6.18.0",
+        "@typescript-eslint/visitor-keys": "6.18.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
+        "minimatch": "9.0.3",
         "semver": "^7.5.4",
         "ts-api-utils": "^1.0.1"
       },
@@ -2295,12 +2337,12 @@
       }
     },
     "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
-      "version": "6.13.2",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz",
-      "integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==",
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz",
+      "integrity": "sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.13.2",
+        "@typescript-eslint/types": "6.18.0",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
@@ -2311,6 +2353,30 @@
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
+    "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/utils/node_modules/minimatch": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/@typescript-eslint/visitor-keys": {
       "version": "6.7.3",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.3.tgz",
@@ -2449,9 +2515,9 @@
       }
     },
     "node_modules/@web-scrobbler/metadata-filter": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@web-scrobbler/metadata-filter/-/metadata-filter-3.0.2.tgz",
-      "integrity": "sha512-rmsjHHLgvLRTF2PunBA7f+jSB/MwPczPU/xnkOwfeOWVOw9jdyHhr8AnLu20K+b702vtgTDb7wkDxMCDP0UohA==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@web-scrobbler/metadata-filter/-/metadata-filter-3.1.0.tgz",
+      "integrity": "sha512-3VLxdrcfRZ/jac7VV3v5s3R+HUQ/o4gp8vqZoojGhrvyrYyrjsyuJGaoSiFLoYmwYNAjeqJ/tnvIeJt8DSi4sg==",
       "engines": {
         "node": ">=10.0.0"
       }
@@ -4673,9 +4739,9 @@
       }
     },
     "node_modules/esbuild": {
-      "version": "0.19.5",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.5.tgz",
-      "integrity": "sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==",
+      "version": "0.19.11",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz",
+      "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==",
       "dev": true,
       "hasInstallScript": true,
       "bin": {
@@ -4685,28 +4751,29 @@
         "node": ">=12"
       },
       "optionalDependencies": {
-        "@esbuild/android-arm": "0.19.5",
-        "@esbuild/android-arm64": "0.19.5",
-        "@esbuild/android-x64": "0.19.5",
-        "@esbuild/darwin-arm64": "0.19.5",
-        "@esbuild/darwin-x64": "0.19.5",
-        "@esbuild/freebsd-arm64": "0.19.5",
-        "@esbuild/freebsd-x64": "0.19.5",
-        "@esbuild/linux-arm": "0.19.5",
-        "@esbuild/linux-arm64": "0.19.5",
-        "@esbuild/linux-ia32": "0.19.5",
-        "@esbuild/linux-loong64": "0.19.5",
-        "@esbuild/linux-mips64el": "0.19.5",
-        "@esbuild/linux-ppc64": "0.19.5",
-        "@esbuild/linux-riscv64": "0.19.5",
-        "@esbuild/linux-s390x": "0.19.5",
-        "@esbuild/linux-x64": "0.19.5",
-        "@esbuild/netbsd-x64": "0.19.5",
-        "@esbuild/openbsd-x64": "0.19.5",
-        "@esbuild/sunos-x64": "0.19.5",
-        "@esbuild/win32-arm64": "0.19.5",
-        "@esbuild/win32-ia32": "0.19.5",
-        "@esbuild/win32-x64": "0.19.5"
+        "@esbuild/aix-ppc64": "0.19.11",
+        "@esbuild/android-arm": "0.19.11",
+        "@esbuild/android-arm64": "0.19.11",
+        "@esbuild/android-x64": "0.19.11",
+        "@esbuild/darwin-arm64": "0.19.11",
+        "@esbuild/darwin-x64": "0.19.11",
+        "@esbuild/freebsd-arm64": "0.19.11",
+        "@esbuild/freebsd-x64": "0.19.11",
+        "@esbuild/linux-arm": "0.19.11",
+        "@esbuild/linux-arm64": "0.19.11",
+        "@esbuild/linux-ia32": "0.19.11",
+        "@esbuild/linux-loong64": "0.19.11",
+        "@esbuild/linux-mips64el": "0.19.11",
+        "@esbuild/linux-ppc64": "0.19.11",
+        "@esbuild/linux-riscv64": "0.19.11",
+        "@esbuild/linux-s390x": "0.19.11",
+        "@esbuild/linux-x64": "0.19.11",
+        "@esbuild/netbsd-x64": "0.19.11",
+        "@esbuild/openbsd-x64": "0.19.11",
+        "@esbuild/sunos-x64": "0.19.11",
+        "@esbuild/win32-arm64": "0.19.11",
+        "@esbuild/win32-ia32": "0.19.11",
+        "@esbuild/win32-x64": "0.19.11"
       }
     },
     "node_modules/escalade": {
@@ -4731,15 +4798,15 @@
       }
     },
     "node_modules/eslint": {
-      "version": "8.55.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
-      "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
+      "version": "8.56.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
+      "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.6.1",
         "@eslint/eslintrc": "^2.1.4",
-        "@eslint/js": "8.55.0",
+        "@eslint/js": "8.56.0",
         "@humanwhocodes/config-array": "^0.11.13",
         "@humanwhocodes/module-importer": "^1.0.1",
         "@nodelib/fs.walk": "^1.2.8",
@@ -4861,14 +4928,13 @@
       }
     },
     "node_modules/eslint-plugin-solid": {
-      "version": "0.13.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-solid/-/eslint-plugin-solid-0.13.0.tgz",
-      "integrity": "sha512-Sutd+DxEGu9+Z9ITtHKXRAClxVe1a6C1XQZSuN8iBsMy0IAVEc6Tca1UYgc7tD2ZrRRjZKB9mohBOaZl5NJLgg==",
+      "version": "0.13.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-solid/-/eslint-plugin-solid-0.13.1.tgz",
+      "integrity": "sha512-PdNrAylFzeh/SbnLc2pQ432l+bXFGzXj/qNqkh5QNVZCoWIdSs0CJA2D7hqW0DloztwUrzkVZCDWFWc3iRAm/Q==",
       "dev": true,
       "dependencies": {
         "@typescript-eslint/utils": "^6.4.0",
         "is-html": "^2.0.0",
-        "jsx-ast-utils": "^3.3.3",
         "kebab-case": "^1.0.2",
         "known-css-properties": "^0.24.0",
         "style-to-object": "^0.3.0"
@@ -9066,9 +9132,9 @@
       }
     },
     "node_modules/prettier": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
-      "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
+      "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
       "dev": true,
       "bin": {
         "prettier": "bin/prettier.cjs"
@@ -10973,9 +11039,9 @@
       }
     },
     "node_modules/sass": {
-      "version": "1.69.5",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz",
-      "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==",
+      "version": "1.69.7",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz",
+      "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==",
       "devOptional": true,
       "dependencies": {
         "chokidar": ">=3.0.0 <4.0.0",
@@ -11058,9 +11124,9 @@
       }
     },
     "node_modules/seroval": {
-      "version": "0.12.3",
-      "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.12.3.tgz",
-      "integrity": "sha512-5WDeMpv7rmEylsypRj1iwRVHE/QLsMLiZ+9savlNNQEVdgGia1iRMb7qyaAagY0wu/7+QTe6d2wldk/lgaLb6g==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.3.tgz",
+      "integrity": "sha512-v5Psun3O2OdehTS/BpTC9EezHxyQRM4lqtvOdbngEq8qOZUECqG5XaoFNZgBQrJKXZFgcyZtMH6Dv7G96pKSSQ==",
       "engines": {
         "node": ">=10"
       }
@@ -11217,12 +11283,12 @@
       }
     },
     "node_modules/solid-js": {
-      "version": "1.8.5",
-      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.5.tgz",
-      "integrity": "sha512-xvtJvzJzWbsn35oKFhW9kNwaxG1Z/YLMsDp4tLVcYZTMPzvzQ8vEZuyDQ6nt7xDArVgZJ7TUFrJUwrui/oq53A==",
+      "version": "1.8.8",
+      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.8.tgz",
+      "integrity": "sha512-9CtL5xWTYX1WWjQKqht3Tl0AJzgz4YWVQk8hoscO9TzRCgzlpAauEOexXa6bPG30W+fWLnFVE7XUiAzQFNeUKw==",
       "dependencies": {
         "csstype": "^3.1.0",
-        "seroval": "^0.12.0"
+        "seroval": "^1.0.2"
       }
     },
     "node_modules/solid-refresh": {
@@ -12447,12 +12513,12 @@
       }
     },
     "node_modules/tsx": {
-      "version": "4.6.2",
-      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.6.2.tgz",
-      "integrity": "sha512-QPpBdJo+ZDtqZgAnq86iY/PD2KYCUPSUGIunHdGwyII99GKH+f3z3FZ8XNFLSGQIA4I365ui8wnQpl8OKLqcsg==",
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.0.tgz",
+      "integrity": "sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==",
       "dev": true,
       "dependencies": {
-        "esbuild": "~0.18.20",
+        "esbuild": "~0.19.10",
         "get-tsconfig": "^4.7.2"
       },
       "bin": {
@@ -12465,395 +12531,6 @@
         "fsevents": "~2.3.3"
       }
     },
-    "node_modules/tsx/node_modules/@esbuild/android-arm": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
-      "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/android-arm64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
-      "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/android-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
-      "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
-      "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
-      "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
-      "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
-      "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/linux-arm": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
-      "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
-      "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
-      "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
-      "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
-      "cpu": [
-        "loong64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
-      "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
-      "cpu": [
-        "mips64el"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
-      "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
-      "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
-      "cpu": [
-        "riscv64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
-      "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
-      "cpu": [
-        "s390x"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/linux-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
-      "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
-      "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
-      "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
-      "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "sunos"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
-      "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
-      "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/@esbuild/win32-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
-      "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/tsx/node_modules/esbuild": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
-      "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
-      "dev": true,
-      "hasInstallScript": true,
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "optionalDependencies": {
-        "@esbuild/android-arm": "0.18.20",
-        "@esbuild/android-arm64": "0.18.20",
-        "@esbuild/android-x64": "0.18.20",
-        "@esbuild/darwin-arm64": "0.18.20",
-        "@esbuild/darwin-x64": "0.18.20",
-        "@esbuild/freebsd-arm64": "0.18.20",
-        "@esbuild/freebsd-x64": "0.18.20",
-        "@esbuild/linux-arm": "0.18.20",
-        "@esbuild/linux-arm64": "0.18.20",
-        "@esbuild/linux-ia32": "0.18.20",
-        "@esbuild/linux-loong64": "0.18.20",
-        "@esbuild/linux-mips64el": "0.18.20",
-        "@esbuild/linux-ppc64": "0.18.20",
-        "@esbuild/linux-riscv64": "0.18.20",
-        "@esbuild/linux-s390x": "0.18.20",
-        "@esbuild/linux-x64": "0.18.20",
-        "@esbuild/netbsd-x64": "0.18.20",
-        "@esbuild/openbsd-x64": "0.18.20",
-        "@esbuild/sunos-x64": "0.18.20",
-        "@esbuild/win32-arm64": "0.18.20",
-        "@esbuild/win32-ia32": "0.18.20",
-        "@esbuild/win32-x64": "0.18.20"
-      }
-    },
     "node_modules/tunnel-agent": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -13979,9 +13656,9 @@
       }
     },
     "node_modules/vite-plugin-static-copy": {
-      "version": "0.17.0",
-      "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-0.17.0.tgz",
-      "integrity": "sha512-2HpNbHfDt8SDy393AGXh9llHkc8FJMQkI8s3T5WsH3SWLMO+f5cFIyPErl4yGKU9Uh3Vaqsd4lHZYTf042fQ2A==",
+      "version": "0.17.1",
+      "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-0.17.1.tgz",
+      "integrity": "sha512-9h3iaVs0bqnqZOM5YHJXGHqdC5VAVlTZ2ARYsuNpzhEJUHmFqXY7dAK4ZFpjEQ4WLFKcaN8yWbczr81n01U4sQ==",
       "dev": true,
       "dependencies": {
         "chokidar": "^3.5.3",
@@ -13993,7 +13670,7 @@
         "node": "^14.18.0 || >=16.0.0"
       },
       "peerDependencies": {
-        "vite": "^3.0.0 || ^4.0.0"
+        "vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
       }
     },
     "node_modules/vite/node_modules/@esbuild/android-arm": {
diff --git a/package.json b/package.json
index 95e2a3db..8bf4782e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "web-scrobbler",
-  "version": "3.3.0",
+  "version": "3.4.0",
   "author": "David Šabata <posta@davidsabata.cz>",
   "contributors": [
     "@mountainash (https://mountainash.id.au/)"
@@ -29,7 +29,7 @@
   "homepage": "https://web-scrobbler.com/",
   "dependencies": {
     "@suid/icons-material": "0.6.11",
-    "@web-scrobbler/metadata-filter": "3.0.2",
+    "@web-scrobbler/metadata-filter": "3.1.0",
     "blueimp-md5": "2.19.0",
     "stylelint": "15.11.0",
     "vite": "4.5.1",
@@ -41,37 +41,37 @@
     "@types/fs-extra": "11.0.3",
     "@types/imagemin": "8.0.5",
     "@types/imagemin-jpegtran": "5.0.4",
-    "@types/node": "20.10.4",
-    "@types/react": "18.2.43",
-    "@types/webextension-polyfill": "0.10.4",
-    "@typescript-eslint/eslint-plugin": "6.13.2",
+    "@types/node": "20.10.6",
+    "@types/react": "18.2.47",
+    "@types/webextension-polyfill": "0.10.7",
+    "@typescript-eslint/eslint-plugin": "6.18.0",
     "@web-scrobbler/eslint-config": "3.0.0",
     "@web-scrobbler/stylelint-config": "1.0.1",
     "archiver": "6.0.1",
     "canvas": "2.11.2",
     "chokidar": "3.5.3",
-    "esbuild": "0.19.5",
-    "eslint": "8.55.0",
+    "esbuild": "0.19.11",
+    "eslint": "8.56.0",
     "eslint-plugin-react": "7.33.2",
-    "eslint-plugin-solid": "0.13.0",
+    "eslint-plugin-solid": "0.13.1",
     "eslint-plugin-tsdoc": "0.2.17",
     "fs-extra": "11.1.1",
     "glob": "10.3.10",
     "imagemin": "8.0.1",
     "imagemin-jpegtran": "7.0.0",
     "imagemin-pngquant": "9.0.2",
-    "prettier": "3.0.3",
+    "prettier": "3.1.1",
     "prettier-config-web-scrobbler": "0.1.0",
     "remark": "15.0.1",
     "remark-cli": "12.0.0",
     "remark-preset-lint-recommended": "6.1.3",
-    "sass": "1.69.5",
-    "solid-js": "1.8.5",
-    "tsx": "4.6.2",
+    "sass": "1.69.7",
+    "solid-js": "1.8.8",
+    "tsx": "4.7.0",
     "typescript": "5.3.3",
     "vite-plugin-conditional-compiler": "0.2.1",
     "vite-plugin-solid": "2.8.0",
-    "vite-plugin-static-copy": "0.17.0",
+    "vite-plugin-static-copy": "0.17.1",
     "vitest": "0.34.6"
   },
   "engines": {
diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index 1b8a6da9..7ef6af04 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -203,6 +203,14 @@
         "message": "Custom API URL",
         "description": "Input label"
     },
+    "instanceDomain": {
+        "message": "Instance Domain",
+        "description": "Input label"
+    },
+    "instanceDomainPlaceholder": {
+        "message": "just domain: example.com",
+        "description": "Input label"
+    },
     "accountsUserToken": {
         "message": "Token",
         "description": "Input label"
@@ -255,6 +263,18 @@
         "message": "$1 regex/bulk edits",
         "description": "'Regex/Bulk edits' header"
     },
+    "optionsBlockedSources": {
+        "message": "Blocked sources",
+        "description": "'Edited tracks' header"
+    },
+    "optionsBlockedSourcesDesc": {
+        "message": "You can block scrobbling from specific sources such as channels on this website.",
+        "description": "Description of blocked sources"
+    },
+    "optionsBlockedSourcesPopupTitle": {
+        "message": "$1 blocked sources",
+        "description": "'Blocked sources' header"
+    },
     "optionsViewEdited": {
         "message": "View",
         "description": "Button to view edited tracks"
@@ -416,7 +436,7 @@
         "description": "'About' section text"
     },
     "aboutExtensionDesc": {
-        "message": "Web Scrobbler is a browser extension created for people who listen to music online through their browser, and would like to keep an updated playback history using scrobbling services, such as Last.fm, Libre.fm and ListenBrainz.",
+        "message": "Web Scrobbler is a browser extension created for people who listen to music online through their browser, and would like to keep an updated playback history using scrobbling services, such as Last.fm, Libre.fm, ListenBrainz, and Pleroma.",
         "description": "'About' section text"
     },
     "contributorsTitle": {
@@ -831,6 +851,14 @@
         "message": "Disable $1 connector until tab is closed",
         "description": "Context menu item"
     },
+    "menuDisableChannel": {
+        "message": "Disable scrobbling from $1",
+        "description": "Context menu item"
+    },
+    "menuEnableChannel": {
+        "message": "Enable scrobbling from $1",
+        "description": "Context menu item"
+    },
     "notificationConnectAccounts": {
         "message": "Connect your accounts",
         "description": "Notification title"
@@ -879,4 +907,4 @@
         "message": "The track is not recognized. Click web scrobbler extension icon to correct info.",
         "description": "Infobox track is not recognized"
     }
-}
\ No newline at end of file
+}
diff --git a/src/connectors/buymusicclub.ts b/src/connectors/buymusicclub.ts
new file mode 100644
index 00000000..65dc29a3
--- /dev/null
+++ b/src/connectors/buymusicclub.ts
@@ -0,0 +1,17 @@
+export {};
+
+Connector.playerSelector = '#player';
+
+Connector.playButtonSelector = '#play-button';
+
+Connector.artistSelector = '#track-artist';
+
+Connector.albumSelector = '#track-album';
+
+Connector.trackSelector = '#track-title';
+
+Connector.trackArtSelector = '#track-art';
+
+Connector.durationSelector = '#duration';
+
+Connector.currentTimeSelector = '#time';
diff --git a/src/connectors/jam.coop.ts b/src/connectors/jam.coop.ts
new file mode 100644
index 00000000..31eccb41
--- /dev/null
+++ b/src/connectors/jam.coop.ts
@@ -0,0 +1,94 @@
+export {};
+
+const trackListSelector = '[data-player-target="track"]';
+const progressSelector = '[data-player-target="progress"]';
+
+/**
+ * Finds the currently played track (as HTMLElement) in the track list.
+ * Contains track URL and duration.
+ */
+const getCurrentTrackListElement = () => {
+	const playingTrackName = Connector.getTrack();
+	if (!playingTrackName) {
+		return null;
+	}
+	const trackListElements = Util.queryElements(trackListSelector);
+	if (!trackListElements) {
+		return null;
+	}
+	for (const trackListElem of trackListElements) {
+		const trackListElemTitle = trackListElem.querySelector(
+			'span.cursor-pointer',
+		);
+		if (trackListElemTitle?.textContent === playingTrackName) {
+			return trackListElem;
+		}
+	}
+	return null;
+};
+
+/**
+ * While playing, a progress bar is updated frequently.
+ * Its width CSS attribute tells us the current duration percentage.
+ * @returns value between 0 and 100
+ */
+const getProgressPercentage = (): number => {
+	const progressElements = Util.queryElements(progressSelector);
+	if (!progressElements || !progressElements.length) {
+		return 0;
+	}
+	const widthCss = progressElements[0].style.width;
+	return parseFloat(widthCss);
+};
+
+Connector.playerSelector = '[data-controller="player"]';
+
+Connector.playButtonSelector = '[data-action="player#play"]';
+Connector.pauseButtonSelector = '[data-action="player#pause"]';
+
+Connector.artistSelector = `${Connector.playerSelector} h2`;
+
+Connector.trackSelector = `${Connector.playerSelector} h3`;
+
+Connector.albumSelector = `${Connector.playerSelector} h1`;
+
+Connector.trackArtSelector = `${Connector.playerSelector} img`;
+
+Connector.isPlaying = () => {
+	const elements = Util.queryElements(Connector.pauseButtonSelector);
+	if (!elements || !elements.length) {
+		return false;
+	}
+	return !elements[0].classList.contains('hidden');
+};
+
+Connector.getUniqueID = () => {
+	const currentTrackListElem = getCurrentTrackListElement();
+	if (!currentTrackListElem) {
+		return null;
+	}
+	return currentTrackListElem.getAttribute('data-track-url');
+};
+
+Connector.getDuration = () => {
+	const currentTrackListElem = getCurrentTrackListElement();
+	if (!currentTrackListElem) {
+		return null;
+	}
+	const durationElem =
+		currentTrackListElem.querySelector('span:nth-child(2)');
+	if (!durationElem) {
+		return null;
+	}
+	return Util.stringToSeconds(durationElem.textContent);
+};
+
+Connector.getCurrentTime = () => {
+	const duration = Connector.getDuration() ?? 0;
+	const percentage = getProgressPercentage();
+	return (duration * percentage) / 100;
+};
+
+// The player (dis)appears dynamically.
+// A viable approach to detect this is to call Connector.onStateChanged() from time to time
+setInterval(Connector.onStateChanged, 1000);
diff --git a/src/connectors/joox.ts b/src/connectors/joox.ts
index 2599b4cc..0d07e2ee 100644
--- a/src/connectors/joox.ts
+++ b/src/connectors/joox.ts
@@ -1,23 +1,9 @@
 export {};
 
-const playerSelector = '.kIpMXu';
-const artistSelector = `${playerSelector} .kOwKMw a`;
-
 Connector.useMediaSessionApi();
 
-Connector.playerSelector = playerSelector;
-
-Connector.trackSelector = '.xXMwE';
-
-Connector.trackArtSelector = `${playerSelector} .XVHIE img`;
-
-Connector.getArtist = () => {
-	const artistNodes = document.querySelectorAll(artistSelector);
-	return Util.joinArtists(Array.from(artistNodes));
-};
+Connector.playerSelector = '#__next > div:nth-child(2) > div:nth-child(2)';
 
 Connector.currentTimeSelector = '#currentTime';
 
-Connector.durationSelector = '.GeFxq';
-
 Connector.playButtonSelector = '.playerIcon--play';
diff --git a/src/connectors/kexp.ts b/src/connectors/kexp.ts
index 02c9f596..6f921e07 100644
--- a/src/connectors/kexp.ts
+++ b/src/connectors/kexp.ts
@@ -1,5 +1,11 @@
 export {};
 
+const filter = MetadataFilter.createFilter({
+	artist: replaceSmartQuotes,
+	track: replaceSmartQuotes,
+	album: replaceSmartQuotes,
+});
+
 Connector.playerSelector = '.Player';
 
 Connector.artistTrackSelector = '.Player-title';
@@ -12,3 +18,9 @@ Connector.isTrackArtDefault = (url) => url?.includes('default');
 
 Connector.isPlaying = () =>
 	Util.hasElementClass(Connector.playerSelector, 'Player--playing');
+
+Connector.applyFilter(filter);
+
+function replaceSmartQuotes(text: string) {
+	return text.replace(/[\u2018\u2019]/g, "'").replace(/[\u201c\u201d]/g, '"');
+}
diff --git a/src/connectors/radiocidade.ts b/src/connectors/radiocidade.ts
new file mode 100644
index 00000000..d29150e6
--- /dev/null
+++ b/src/connectors/radiocidade.ts
@@ -0,0 +1,11 @@
+export {};
+
+Connector.playerSelector = '.musica-info-total';
+
+Connector.artistSelector = '.nome-artista';
+
+Connector.trackSelector = '.nome-musica';
+
+Connector.trackArtSelector = '#capa-album';
+
+Connector.isPlaying = () => Util.hasElementClass('#botao-play', 'tocando');
diff --git a/src/connectors/radiostudent.ts b/src/connectors/radiostudent.ts
new file mode 100644
index 00000000..52ead07b
--- /dev/null
+++ b/src/connectors/radiostudent.ts
@@ -0,0 +1,4 @@
+export {};
+
+Connector.playerSelector = '#icecast_list';
+Connector.artistTrackSelector = '#icecast_list > ul > li > p';
diff --git a/src/connectors/siriusxm-player.ts b/src/connectors/siriusxm-player.ts
index 811b9e6c..7948ef44 100644
--- a/src/connectors/siriusxm-player.ts
+++ b/src/connectors/siriusxm-player.ts
@@ -15,20 +15,19 @@ const filter = MetadataFilter.createFilter({
 	track: [removeYear, removeCover, removeExclusive],
 });
 
-Connector.playerSelector = '.sxm-player-controls';
-Connector.artistSelector = '.sxm-player-controls .artist-name';
-Connector.trackSelector = '.sxm-player-controls .track-name';
+Connector.playerSelector = "*[class^='_playbackControls_']";
 
-Connector.isPlaying = () => {
-	return (
-		Util.getAttrFromSelectors(
-			'.sxm-player-controls .play-pause-btn',
-			'title',
-		) === 'Pause'
-	);
-};
+Connector.artistTrackSelector =
+	"*[class^='_playbackControls_'] *[class*='_title_']";
+
+Connector.trackArtSelector =
+	"*[class^='_playbackControls_'] *[class^='_trackImage_'] img[class^='_image-image']";
+
+Connector.playButtonSelector =
+	"*[class^='_playbackControls_'] button[aria-label='Play']";
 
-Connector.trackArtSelector = '.album-image-cell img.album-image';
+Connector.pauseButtonSelector =
+	"*[class^='_playbackControls_'] button[aria-label='Pause']";
 
 Connector.isScrobblingAllowed = () => {
 	const artist = Connector.getArtist()?.toLowerCase();
diff --git a/src/connectors/youtube-music.ts b/src/connectors/youtube-music.ts
index 2e2e1d5e..a42496a4 100644
--- a/src/connectors/youtube-music.ts
+++ b/src/connectors/youtube-music.ts
@@ -104,12 +104,14 @@ Connector.isPlaying = () => {
 };
 
 Connector.getUniqueID = () => {
-	const videoUrl = Util.getAttrFromSelectors('.yt-uix-sessionlink', 'href');
+	const uniqueId = new URLSearchParams(window.location.search).get('v');
 
-	if (videoUrl) {
-		return Util.getYtVideoIdFromUrl(videoUrl);
+	if (uniqueId) {
+		return uniqueId;
 	}
-	return null;
+
+	const videoUrl = Util.getAttrFromSelectors('.yt-uix-sessionlink', 'href');
+	return Util.getYtVideoIdFromUrl(videoUrl);
 };
 
 Connector.isScrobblingAllowed = () => !Util.isElementVisible(adSelector);
diff --git a/src/connectors/youtube.ts b/src/connectors/youtube.ts
index e66a4ab5..f96d815b 100644
--- a/src/connectors/youtube.ts
+++ b/src/connectors/youtube.ts
@@ -93,6 +93,15 @@ Connector.scrobbleInfoStyle = {
 	fontWeight: '700',
 };
 
+Connector.getChannelId = () =>
+	new URL(
+		(
+			document.querySelector(
+				'#upload-info .ytd-channel-name .yt-simple-endpoint',
+			) as HTMLAnchorElement
+		)?.href ?? 'https://youtube.com/',
+	).pathname.slice(2);
+
 Connector.getTrackInfo = () => {
 	const trackInfo: TrackInfoWithAlbum = {};
 
diff --git a/src/core/background/action.ts b/src/core/background/action.ts
index fdda163b..632327e1 100644
--- a/src/core/background/action.ts
+++ b/src/core/background/action.ts
@@ -1,5 +1,10 @@
 import browser from 'webextension-polyfill';
-import { contextMenus, getBrowserPreferredTheme } from './util';
+import {
+	contextMenus,
+	getBrowserPreferredTheme,
+	getChannelDetails,
+	isChannelBlocklisted,
+} from './util';
 import { sendPopupMessage } from '@/util/communication';
 import { ManagerTab } from '@/core/storage/wrapper';
 import * as ControllerMode from '@/core/object/controller/controller-mode';
@@ -40,6 +45,12 @@ async function updateMenus(tab: ManagerTab): Promise<void> {
 		browser.contextMenus?.update(contextMenus.DISABLE_UNTIL_CLOSED, {
 			visible: false,
 		});
+		browser.contextMenus?.update(contextMenus.DISABLE_CHANNEL, {
+			visible: false,
+		});
+		browser.contextMenus?.update(contextMenus.ENABLE_CHANNEL, {
+			visible: false,
+		});
 		return;
 	}
 
@@ -56,6 +67,12 @@ async function updateMenus(tab: ManagerTab): Promise<void> {
 		browser.contextMenus?.update(contextMenus.DISABLE_UNTIL_CLOSED, {
 			visible: false,
 		});
+		browser.contextMenus?.update(contextMenus.DISABLE_CHANNEL, {
+			visible: false,
+		});
+		browser.contextMenus?.update(contextMenus.ENABLE_CHANNEL, {
+			visible: false,
+		});
 		return;
 	}
 
@@ -70,6 +87,41 @@ async function updateMenus(tab: ManagerTab): Promise<void> {
 		visible: true,
 		title: t('menuDisableUntilTabClosed', connector?.label),
 	});
+
+	const channelDetails = await getChannelDetails(tab.tabId);
+	if (
+		!channelDetails ||
+		!channelDetails.channelId ||
+		!channelDetails.connector
+	) {
+		browser.contextMenus?.update(contextMenus.DISABLE_CHANNEL, {
+			visible: false,
+		});
+		browser.contextMenus?.update(contextMenus.ENABLE_CHANNEL, {
+			visible: false,
+		});
+	} else if (
+		await isChannelBlocklisted(
+			channelDetails.channelId,
+			channelDetails.connector,
+		)
+	) {
+		browser.contextMenus?.update(contextMenus.DISABLE_CHANNEL, {
+			visible: false,
+		});
+		browser.contextMenus?.update(contextMenus.ENABLE_CHANNEL, {
+			visible: true,
+			title: t('menuEnableChannel', channelDetails.channelId),
+		});
+	} else {
+		browser.contextMenus?.update(contextMenus.DISABLE_CHANNEL, {
+			visible: true,
+			title: t('menuDisableChannel', channelDetails.channelId),
+		});
+		browser.contextMenus?.update(contextMenus.ENABLE_CHANNEL, {
+			visible: false,
+		});
+	}
 }
 
 /**
diff --git a/src/core/background/main.ts b/src/core/background/main.ts
index b0d21d0c..553eb786 100644
--- a/src/core/background/main.ts
+++ b/src/core/background/main.ts
@@ -21,6 +21,8 @@ import {
 	getCurrentTab,
 	getActiveTabDetails,
 	getCurrentTabId,
+	addToBlocklist,
+	removeFromBlocklist,
 } from './util';
 import {
 	ControllerModeStr,
@@ -529,6 +531,20 @@ function onStartup() {
 		contexts: ['action'],
 		title: 'Error: You should not be seeing this',
 	});
+
+	browser.contextMenus.create({
+		id: contextMenus.ENABLE_CHANNEL,
+		visible: false,
+		contexts: ['action'],
+		title: 'Error: You should not be seeing this',
+	});
+
+	browser.contextMenus.create({
+		id: contextMenus.DISABLE_CHANNEL,
+		visible: false,
+		contexts: ['action'],
+		title: 'Error: You should not be seeing this',
+	});
 }
 
 /**
@@ -561,5 +577,13 @@ async function contextMenuHandler(info: browser.Menus.OnClickData) {
 			disableUntilClosed(tab.tabId);
 			break;
 		}
+		case contextMenus.ENABLE_CHANNEL: {
+			removeFromBlocklist(tab.tabId);
+			break;
+		}
+		case contextMenus.DISABLE_CHANNEL: {
+			addToBlocklist(tab.tabId);
+			break;
+		}
 	}
 }
diff --git a/src/core/background/util.ts b/src/core/background/util.ts
index e4ccfcf0..5de15fe5 100644
--- a/src/core/background/util.ts
+++ b/src/core/background/util.ts
@@ -6,13 +6,17 @@ import * as BrowserStorage from '@/core/storage/browser-storage';
 import { isPrioritizedMode } from '@/core/object/controller/controller';
 import { performUpdateAction } from './action';
 import { sendBackgroundMessage } from '@/util/communication';
+import { ConnectorMeta } from '../connectors';
 
 const state = BrowserStorage.getStorage(BrowserStorage.STATE_MANAGEMENT);
+const blocklistStorage = BrowserStorage.getStorage(BrowserStorage.BLOCKLISTS);
 
 export const contextMenus = {
 	ENABLE_CONNECTOR: 'enableConnector',
 	DISABLE_CONNECTOR: 'disableConnector',
 	DISABLE_UNTIL_CLOSED: 'disableUntilClosed',
+	ENABLE_CHANNEL: 'enableChannel',
+	DISABLE_CHANNEL: 'disableChannel',
 };
 
 /**
@@ -61,6 +65,32 @@ export async function filterInactiveTabs(activeTabs: ManagerTab[]) {
 	});
 }
 
+/**
+ * @param tabId - tab to get song from
+ * @returns the details about the channel of the currently playing song in tab
+ */
+export async function getChannelDetails(tabId: number) {
+	return sendBackgroundMessage(tabId, {
+		type: 'getChannelDetails',
+		payload: undefined,
+	});
+}
+
+/**
+ * Is current channel blocklisted
+ *
+ * @param channelId - ID of the channel to check
+ * @param connector - Details about the connector to check
+ * @returns true if current channel is blocklisted; false otherwise
+ */
+export async function isChannelBlocklisted(
+	channelId: string,
+	connector: ConnectorMeta,
+): Promise<boolean> {
+	const blocklist = await blocklistStorage.get();
+	return blocklist?.[connector.id]?.[channelId] ?? false;
+}
+
 /**
  * Unlock state management storage.
  */
@@ -236,6 +266,30 @@ export function enableConnector(tabId: number) {
 	});
 }
 
+/**
+ * Disables scrobbling current channel for a tab
+ *
+ * @param tabId - tab id of tab to disable scrobbling channel for
+ */
+export function addToBlocklist(tabId: number) {
+	sendBackgroundMessage(tabId, {
+		type: 'addToBlocklist',
+		payload: undefined,
+	});
+}
+
+/**
+ * Enables scrobbling current channel for a tab
+ *
+ * @param tabId - tab id of tab to disable scrobbling channel for
+ */
+export function removeFromBlocklist(tabId: number) {
+	sendBackgroundMessage(tabId, {
+		type: 'removeFromBlocklist',
+		payload: undefined,
+	});
+}
+
 /**
  * Disables connector of a tab until the tab is closed
  *
diff --git a/src/core/connectors.ts b/src/core/connectors.ts
index 07215a36..b3982a00 100644
--- a/src/core/connectors.ts
+++ b/src/core/connectors.ts
@@ -4,6 +4,11 @@ export interface ConnectorMeta {
 	js: string;
 	id: string;
 	allFrames?: true;
+
+	/**
+	 * true if connector uses blocklist. Connector must implement {@link Connector.getChannelId}
+	 */
+	usesBlocklist?: true;
 }
 
 export default <ConnectorMeta[]>[
@@ -12,6 +17,7 @@ export default <ConnectorMeta[]>[
 		matches: ['*://www.youtube.com/*', '*://m.youtube.com/*'],
 		js: 'youtube.js',
 		id: 'youtube',
+		usesBlocklist: true,
 	},
 	{
 		label: 'MySpace',
@@ -39,6 +45,12 @@ export default <ConnectorMeta[]>[
 		id: 'bndcmpr',
 		allFrames: true,
 	},
+	{
+		label: 'Buy Music Club',
+		matches: ['*://www.buymusic.club/*'],
+		js: 'buymusicclub.js',
+		id: 'buymusicclub',
+	},
 	{
 		label: 'Pandora',
 		matches: ['*://www.pandora.com/*'],
@@ -138,6 +150,17 @@ export default <ConnectorMeta[]>[
 		js: '8tracks.js',
 		id: '8tracks',
 	},
+	{
+		label: 'Radio Cidade',
+		matches: [
+			'*://radiocidade.fm/player/*',
+			'*://play.radiomania.com.br/*',
+			'*://*.melodia.com.br/*',
+			'*://jb.fm/player/*',
+		],
+		js: 'radiocidade.js',
+		id: 'radiocidade',
+	},
 	{
 		label: 'Radio Nova',
 		matches: ['*://www.nova.fr/*'],
@@ -1038,7 +1061,7 @@ export default <ConnectorMeta[]>[
 	},
 	{
 		label: 'SiriusXM',
-		matches: ['*://player.siriusxm.com/*', '*://player.siriusxm.ca/*'],
+		matches: ['*://www.siriusxm.com/*', '*://www.siriusxm.ca/*'],
 		js: 'siriusxm-player.js',
 		id: 'siriusxm-player',
 	},
@@ -2300,4 +2323,16 @@ export default <ConnectorMeta[]>[
 		js: 'vk-save.js',
 		id: 'vk-save',
 	},
+	{
+		label: 'Radio Student',
+		matches: ['*://www.radiostudent.hr/*'],
+		js: 'radiostudent.js',
+		id: 'radiostudent',
+	},
+	{
+		label: 'jam.coop',
+		matches: ['*://jam.coop/*'],
+		js: 'jam.coop.js',
+		id: 'jam.coop',
+	},
 ];
diff --git a/src/core/content/connector.ts b/src/core/content/connector.ts
index 710f9d83..a70d7f8c 100644
--- a/src/core/content/connector.ts
+++ b/src/core/content/connector.ts
@@ -144,6 +144,14 @@ export default class BaseConnector {
 		alignItems: 'center',
 	};
 
+	/**
+	 * Function that gets a unique ID for channel/user blocklist.
+	 *
+	 * Has to be specified if usesBlocklist is set to true in connectors.ts.
+	 * If connectors.ts does not have usesBlocklist set to true, this should be null.
+	 */
+	public getChannelId: (() => string | null | undefined) | null = null;
+
 	/**
 	 * Selector of element contains a track art of now playing song.
 	 * Default implementation looks for track art URL in `src` attribute or
diff --git a/src/core/object/cloned-song.ts b/src/core/object/cloned-song.ts
index 0baf97bb..70e7581a 100644
--- a/src/core/object/cloned-song.ts
+++ b/src/core/object/cloned-song.ts
@@ -18,6 +18,7 @@ export default class ClonedSong extends BaseSong {
 	public flags: Flags;
 	public metadata: Metadata;
 	public connectorLabel: string;
+	public connectorId: string;
 	public controllerTabId: number;
 
 	constructor(song: CloneableSong, controllerTabId: number) {
@@ -28,6 +29,7 @@ export default class ClonedSong extends BaseSong {
 		this.flags = song.flags;
 		this.metadata = song.metadata;
 		this.connectorLabel = song.connectorLabel;
+		this.connectorId = song.connectorId;
 		this.controllerTabId = controllerTabId;
 	}
 
diff --git a/src/core/object/controller/controller.ts b/src/core/object/controller/controller.ts
index 3382c562..e67380b4 100644
--- a/src/core/object/controller/controller.ts
+++ b/src/core/object/controller/controller.ts
@@ -25,6 +25,7 @@ import * as BrowserStorage from '@/core/storage/browser-storage';
 import { debugLog } from '@/core/content/util';
 import browser from 'webextension-polyfill';
 import BaseConnector from '@/core/content/connector';
+import Blocklist from '@/core/storage/blocklist';
 
 /**
  * List of song fields used to check if song is changed. If any of
@@ -72,15 +73,10 @@ export default class Controller {
 	private currentSong: Song | null = null;
 	private isReplayingSong = false;
 	private shouldScrobblePodcasts = true;
+	private blocklist: Blocklist;
 
 	private forceScrobble = false;
 	private shouldHaveScrobbled = false;
-	private shouldScrobble = () => {
-		if (this.forceScrobble) {
-			return true;
-		}
-		return this.currentSong?.parsed.isScrobblingAllowed;
-	};
 
 	private isEditing = false;
 	private setNotEditingTimeout = setTimeout(() => {
@@ -92,6 +88,21 @@ export default class Controller {
 		payload: undefined,
 	});
 
+	/**
+	 * @returns true if song should scrobble; false if disallowed.
+	 */
+	private async shouldScrobble() {
+		if (this.forceScrobble) {
+			return true;
+		}
+		return (
+			this.currentSong?.parsed.isScrobblingAllowed &&
+			(await this.blocklist.shouldScrobbleChannel(
+				this.connector.getChannelId?.(),
+			))
+		);
+	}
+
 	/**
 	 * Function that handles updating the scrobble info box
 	 */
@@ -192,6 +203,7 @@ export default class Controller {
 	 */
 	constructor(connector: BaseConnector, isEnabled: boolean) {
 		this.connector = connector;
+		this.blocklist = new Blocklist(this.connector.meta.id);
 		this.isEnabled = isEnabled;
 		this.mode = isEnabled ? ControllerMode.Base : ControllerMode.Disabled;
 		this.setMode(this.mode);
@@ -285,6 +297,35 @@ export default class Controller {
 					}
 				},
 			}),
+			contentListener({
+				type: 'getChannelDetails',
+				fn: () => ({
+					connector: this.connector.meta,
+					channelId: this.connector.getChannelId?.(),
+				}),
+			}),
+			contentListener({
+				type: 'addToBlocklist',
+				fn: async () => {
+					await this.blocklist.addToBlocklist(
+						this.connector.getChannelId?.() ?? '',
+					);
+					this.setMode(ControllerMode.Disallowed);
+				},
+			}),
+			contentListener({
+				type: 'removeFromBlocklist',
+				fn: async () => {
+					await this.blocklist.removeFromBlocklist(
+						this.connector.getChannelId?.() ?? '',
+					);
+					if (this.shouldHaveScrobbled) {
+						void this.scrobbleSong();
+					} else {
+						void this.setSongNowPlaying();
+					}
+				},
+			}),
 		);
 	}
 
@@ -321,7 +362,11 @@ export default class Controller {
 		switch (event) {
 			case ControllerEvents.SongNowPlaying: {
 				const song = this.getCurrentSong();
-				if (!song || song.flags.isReplaying || !this.shouldScrobble()) {
+				if (
+					!song ||
+					song.flags.isReplaying ||
+					!(await this.shouldScrobble())
+				) {
 					break;
 				}
 				const id = await this.tabId;
@@ -352,7 +397,7 @@ export default class Controller {
 			}
 			case ControllerEvents.SongUnrecognized: {
 				const song = this.getCurrentSong();
-				if (!song || !this.shouldScrobble()) {
+				if (!song || !(await this.shouldScrobble())) {
 					break;
 				}
 				const id = await this.tabId;
@@ -649,7 +694,7 @@ export default class Controller {
 	 * Process connector state as current one.
 	 * @param newState - Connector state
 	 */
-	private processCurrentState(newState: State): void {
+	private async processCurrentState(newState: State): Promise<void> {
 		if (!assertSongNotNull(this.currentSong)) {
 			return;
 		}
@@ -680,7 +725,7 @@ export default class Controller {
 			this.onPlayingStateChanged(isPlaying);
 		} else if (
 			this.mode === ControllerMode.Disallowed &&
-			this.shouldScrobble() &&
+			(await this.shouldScrobble()) &&
 			isPlaying
 		) {
 			// we need to unset disallowed whenever needed.
@@ -709,7 +754,7 @@ export default class Controller {
 			return;
 		}
 
-		if (this.shouldScrobble()) {
+		if (await this.shouldScrobble()) {
 			this.setMode(ControllerMode.Loading);
 		} else {
 			this.setMode(ControllerMode.Disallowed);
@@ -738,7 +783,7 @@ export default class Controller {
 			 * If the song is playing, mark it immediately;
 			 * otherwise will be flagged in isPlaying binding.
 			 */
-			if (!this.shouldScrobble()) {
+			if (!(await this.shouldScrobble())) {
 				this.setMode(ControllerMode.Disallowed);
 			} else if (this.currentSong.parsed.isPlaying) {
 				/*
@@ -784,7 +829,7 @@ export default class Controller {
 	 * Called when playing state is changed.
 	 * @param value - New playing state
 	 */
-	private onPlayingStateChanged(value: boolean | null): void {
+	private async onPlayingStateChanged(value: boolean | null): Promise<void> {
 		this.debugLog(`isPlaying state changed to ${String(value)}`);
 
 		if (value && this.currentSong) {
@@ -798,7 +843,7 @@ export default class Controller {
 			if (
 				!isMarkedAsPlaying &&
 				this.currentSong.isValid() &&
-				this.shouldScrobble()
+				(await this.shouldScrobble())
 			) {
 				void this.setSongNowPlaying();
 			} else {
@@ -906,7 +951,10 @@ export default class Controller {
 	 * now playing.
 	 */
 	private async setSongNowPlaying(): Promise<void> {
-		if (!assertSongNotNull(this.currentSong) || !this.shouldScrobble()) {
+		if (
+			!assertSongNotNull(this.currentSong) ||
+			!(await this.shouldScrobble())
+		) {
 			return;
 		}
 		this.currentSong.flags.isMarkedAsPlaying = true;
@@ -930,7 +978,10 @@ export default class Controller {
 	}
 
 	private async setPaused(): Promise<void> {
-		if (!assertSongNotNull(this.currentSong) || !this.shouldScrobble()) {
+		if (
+			!assertSongNotNull(this.currentSong) ||
+			!(await this.shouldScrobble())
+		) {
 			return;
 		}
 		await sendContentMessage({
@@ -942,7 +993,10 @@ export default class Controller {
 	}
 
 	private async setResumedPlaying(): Promise<void> {
-		if (!assertSongNotNull(this.currentSong) || !this.shouldScrobble()) {
+		if (
+			!assertSongNotNull(this.currentSong) ||
+			!(await this.shouldScrobble())
+		) {
 			return;
 		}
 		await sendContentMessage({
@@ -997,7 +1051,7 @@ export default class Controller {
 			return;
 		}
 
-		if (!this.shouldScrobble()) {
+		if (!(await this.shouldScrobble())) {
 			this.shouldHaveScrobbled = true;
 			return;
 		}
diff --git a/src/core/object/scrobble-service.ts b/src/core/object/scrobble-service.ts
index 8117a410..d4d35fe1 100644
--- a/src/core/object/scrobble-service.ts
+++ b/src/core/object/scrobble-service.ts
@@ -4,6 +4,7 @@ import LibreFmScrobbler from '@/core/scrobbler/librefm-scrobbler';
 import ListenBrainzScrobbler from '@/core/scrobbler/listenbrainz/listenbrainz-scrobbler';
 import MalojaScrobbler from '@/core/scrobbler/maloja/maloja-scrobbler';
 import WebhookScrobbler from '@/core/scrobbler/webhook-scrobbler';
+import PleromaScrobbler from '@/core/scrobbler/pleroma/pleroma-scrobbler';
 import { ServiceCallResult } from '@/core/object/service-call-result';
 import { BaseSong } from '@/core/object/song';
 import { ScrobblerSongInfo } from '@/core/scrobbler/base-scrobbler';
@@ -19,7 +20,8 @@ export type Scrobbler =
 	| LibreFmScrobbler
 	| ListenBrainzScrobbler
 	| MalojaScrobbler
-	| WebhookScrobbler;
+	| WebhookScrobbler
+	| PleromaScrobbler;
 
 /**
  * Scrobblers that are registered and that can be bound.
@@ -30,6 +32,7 @@ const registeredScrobblers = [
 	new ListenBrainzScrobbler(),
 	new MalojaScrobbler(),
 	new WebhookScrobbler(),
+	new PleromaScrobbler(),
 ];
 
 export type ScrobblerLabel =
@@ -37,7 +40,8 @@ export type ScrobblerLabel =
 	| 'ListenBrainz'
 	| 'Libre.fm'
 	| 'Maloja'
-	| 'Webhook';
+	| 'Webhook'
+	| 'Pleroma';
 
 /**
  * Check if scrobbler is in given array of scrobblers.
diff --git a/src/core/object/song.ts b/src/core/object/song.ts
index a7e94e23..b5d36351 100644
--- a/src/core/object/song.ts
+++ b/src/core/object/song.ts
@@ -60,6 +60,7 @@ export interface CloneableSong {
 	flags: Flags;
 	metadata: Metadata;
 	connectorLabel: string;
+	connectorId: string;
 }
 
 export abstract class BaseSong {
@@ -69,6 +70,7 @@ export abstract class BaseSong {
 	public abstract flags: Flags;
 	public abstract metadata: Metadata;
 	public abstract connectorLabel: string;
+	public abstract connectorId: string;
 
 	/**
 	 * Get song artist.
@@ -226,6 +228,7 @@ export abstract class BaseSong {
 			metadata: this.metadata,
 			flags: this.flags,
 			connectorLabel: this.connectorLabel,
+			connectorId: this.connectorId,
 		};
 	}
 
@@ -302,6 +305,7 @@ export default class Song extends BaseSong {
 	public flags: Flags;
 	public metadata: Metadata;
 	public connectorLabel: string;
+	public connectorId: string;
 	/**
 	 * @param parsedData - Current state received from connector
 	 * @param connector - Connector match object
@@ -365,6 +369,7 @@ export default class Song extends BaseSong {
 		};
 
 		this.connectorLabel = connector.label;
+		this.connectorId = connector.id;
 
 		this.initSongData();
 	}
diff --git a/src/core/scrobbler/base-scrobbler.ts b/src/core/scrobbler/base-scrobbler.ts
index ae47247f..725ecb10 100644
--- a/src/core/scrobbler/base-scrobbler.ts
+++ b/src/core/scrobbler/base-scrobbler.ts
@@ -378,7 +378,8 @@ export default abstract class BaseScrobbler<K extends keyof ScrobblerModels> {
 		| 'ListenBrainz'
 		| 'Maloja'
 		| 'Libre.fm'
-		| 'Webhook';
+		| 'Webhook'
+		| 'Pleroma';
 
 	/**
 	 * Get URL to profile page.
diff --git a/src/core/scrobbler/listenbrainz/listenbrainz-scrobbler.ts b/src/core/scrobbler/listenbrainz/listenbrainz-scrobbler.ts
index 115d73f6..6d876db5 100644
--- a/src/core/scrobbler/listenbrainz/listenbrainz-scrobbler.ts
+++ b/src/core/scrobbler/listenbrainz/listenbrainz-scrobbler.ts
@@ -278,9 +278,8 @@ export default class ListenBrainzScrobbler extends BaseScrobbler<'ListenBrainz'>
 		}
 
 		if (sessionID && requestInfo.headers) {
-			(
-				requestInfo.headers as Record<string, string>
-			).Authorization = `Token ${sessionID}`;
+			(requestInfo.headers as Record<string, string>).Authorization =
+				`Token ${sessionID}`;
 		}
 		const promise = fetch(url, requestInfo);
 		const timeout = this.REQUEST_TIMEOUT;
@@ -354,7 +353,7 @@ export default class ListenBrainzScrobbler extends BaseScrobbler<'ListenBrainz'>
 					payload: {
 						url,
 					},
-			  });
+				});
 		const timeout = this.REQUEST_TIMEOUT;
 
 		// @ts-expect-error typescript is confused by the combination of ternary and promise wrapped promise. It's a skill issue on typescript's part.
diff --git a/src/core/scrobbler/pleroma/pleroma-scrobbler.ts b/src/core/scrobbler/pleroma/pleroma-scrobbler.ts
new file mode 100644
index 00000000..fc3801d6
--- /dev/null
+++ b/src/core/scrobbler/pleroma/pleroma-scrobbler.ts
@@ -0,0 +1,174 @@
+'use strict';
+
+import { ServiceCallResult } from '@/core/object/service-call-result';
+import { BaseSong } from '@/core/object/song';
+import { timeoutPromise } from '@/util/util';
+import BaseScrobbler, { SessionData } from '@/core/scrobbler/base-scrobbler';
+import {
+	PleromaTrackMetadata,
+	PleromaUser,
+} from '@/core/scrobbler/pleroma/pleroma.types';
+
+/**
+ * Module for communication with Pleroma
+ */
+
+export default class PleromaScrobbler extends BaseScrobbler<'Pleroma'> {
+	public userToken!: string;
+	public userApiUrl!: string;
+	public isLocalOnly = false;
+
+	/** @override */
+	protected getStorageName(): 'Pleroma' {
+		return 'Pleroma';
+	}
+
+	/** @override */
+	public getLabel(): 'Pleroma' {
+		return 'Pleroma';
+	}
+
+	/** @override */
+	public getStatusUrl(): string {
+		return '';
+	}
+
+	/** @override */
+	public getUserDefinedProperties(): string[] {
+		return ['userApiUrl', 'userToken'];
+	}
+
+	/** @override */
+	public async getProfileUrl(): Promise<string> {
+		return Promise.resolve('');
+	}
+
+	/** @override */
+	public async getAuthUrl(): Promise<string> {
+		return Promise.resolve('');
+	}
+	/** @override */
+	protected getBaseProfileUrl(): string {
+		return '';
+	}
+
+	/** @override */
+	public getSongInfo(): Promise<Record<string, never>> {
+		return Promise.resolve({});
+	}
+
+	public toggleLove(): Promise<ServiceCallResult> {
+		return Promise.resolve(ServiceCallResult.ERROR_OTHER);
+	}
+
+	/** @override */
+	public async getSession(): Promise<SessionData> {
+		if (!this.userToken) {
+			throw ServiceCallResult.ERROR_AUTH;
+		}
+
+		const requestInfo = {
+			method: 'GET',
+			headers: {
+				Authorization: `Bearer ${this.userToken}`,
+			},
+		};
+
+		const verifyCredentialsUrl = `https://${this.userApiUrl}/api/v1/accounts/verify_credentials`;
+		const promise = fetch(verifyCredentialsUrl, requestInfo);
+		const timeout = this.REQUEST_TIMEOUT;
+		let response = null;
+		try {
+			response = await timeoutPromise(timeout, promise);
+			if (response.status !== 200) {
+				throw ServiceCallResult.ERROR_AUTH;
+			}
+
+			const json = (await response.json()) as PleromaUser | null;
+			const fqn = json ? json.fqn : '';
+			return { sessionID: this.userToken, sessionName: fqn };
+		} catch (e) {
+			this.debugLog('Error while sending request', 'error');
+			throw ServiceCallResult.ERROR_AUTH;
+		}
+	}
+
+	/** @override */
+	public isReadyForGrantAccess(): Promise<boolean> {
+		return Promise.resolve(false);
+	}
+
+	/** @override */
+	public sendNowPlaying(): Promise<ServiceCallResult> {
+		// Pleroma does not support "now playing" scrobbles.
+		return Promise.resolve(ServiceCallResult.RESULT_OK);
+	}
+
+	/** @override */
+	async sendResumedPlaying(): Promise<ServiceCallResult> {
+		return Promise.resolve(ServiceCallResult.RESULT_OK);
+	}
+
+	/** @override */
+	async sendPaused(): Promise<ServiceCallResult> {
+		return Promise.resolve(ServiceCallResult.RESULT_OK);
+	}
+
+	/** @override */
+	public async scrobble(song: BaseSong): Promise<ServiceCallResult> {
+		const songData = this.makeTrackMetadata(song);
+
+		return this.sendRequest(songData, this.userToken);
+	}
+
+	/** Private methods */
+
+	async sendRequest(
+		params: PleromaTrackMetadata,
+		userToken: string,
+	): Promise<ServiceCallResult> {
+		const requestInfo = {
+			method: 'POST',
+			headers: {
+				'Content-Type': 'application/json',
+				Authorization: `Bearer ${userToken}`,
+			},
+			body: JSON.stringify(params),
+		};
+
+		const scrobbleUrl = `https://${this.userApiUrl}/api/v1/pleroma/scrobble`;
+
+		const promise = fetch(scrobbleUrl, requestInfo);
+
+		const timeout = this.REQUEST_TIMEOUT;
+
+		let response = null;
+		try {
+			response = await timeoutPromise(timeout, promise);
+			if (response.status !== 200) {
+				return ServiceCallResult.ERROR_OTHER;
+			}
+		} catch (e) {
+			this.debugLog('Error while sending request', 'error');
+			return ServiceCallResult.ERROR_OTHER;
+		}
+
+		return ServiceCallResult.RESULT_OK;
+	}
+
+	private makeTrackMetadata(song: BaseSong) {
+		const trackMeta: PleromaTrackMetadata = {
+			artist: song.getArtist() ?? '',
+			title: song.getTrack() ?? '',
+			length: song.getDuration(),
+			url: song.parsed.originUrl ?? '',
+		};
+
+		const album = song.getAlbum();
+		if (album) {
+			trackMeta.album = album;
+		}
+
+		return trackMeta;
+	}
+}
diff --git a/src/core/scrobbler/pleroma/pleroma.types.ts b/src/core/scrobbler/pleroma/pleroma.types.ts
new file mode 100644
index 00000000..c74ecdb8
--- /dev/null
+++ b/src/core/scrobbler/pleroma/pleroma.types.ts
@@ -0,0 +1,15 @@
+export interface PleromaTrackMetadata {
+	artist: string;
+	title: string;
+	url: string;
+	length?: number | null;
+
+	key?: string;
+	album?: string;
+	albumartists?: string[];
+}
+
+export interface PleromaUser {
+	id: string;
+	fqn: string;
+}
diff --git a/src/core/storage/blocklist.ts b/src/core/storage/blocklist.ts
new file mode 100644
index 00000000..e7b1cb6d
--- /dev/null
+++ b/src/core/storage/blocklist.ts
@@ -0,0 +1,95 @@
+import * as BrowserStorage from '@/core/storage/browser-storage';
+
+export default class Blocklist {
+	private storage = BrowserStorage.getStorage(BrowserStorage.BLOCKLISTS);
+	private connectorId: string;
+	private isReady: Promise<true>;
+
+	constructor(connectorId: string) {
+		this.connectorId = connectorId;
+		this.isReady = this.init();
+	}
+
+	/**
+	 * Sets default values to blocklist if necessary
+	 */
+	private async setupDefaultBlocklist() {
+		let data = await this.storage.getLocking();
+		if (!data) {
+			data = {};
+		}
+		if (!(this.connectorId in data) || !data[this.connectorId]) {
+			data[this.connectorId] = {};
+		}
+		await this.storage.setLocking(data);
+	}
+
+	/**
+	 * Initializes the blocklist with default values
+	 */
+	private async init(): Promise<true> {
+		await this.setupDefaultBlocklist();
+		return true;
+	}
+
+	/**
+	 * Adds a channel ID to blocklist
+	 *
+	 * @param id - ID of channel to add
+	 */
+	public async addToBlocklist(id: string): Promise<void> {
+		if (!id) {
+			return;
+		}
+		await this.isReady;
+		const data = await this.storage.getLocking();
+		if (!data || !(this.connectorId in data) || !data[this.connectorId]) {
+			this.storage.unlock();
+			return;
+		}
+		data[this.connectorId] = {
+			...data[this.connectorId],
+			[id]: true,
+		};
+		await this.storage.setLocking(data);
+	}
+
+	/**
+	 * Removes a channel ID from blocklist
+	 *
+	 * @param id - ID of channel to remove
+	 */
+	public async removeFromBlocklist(id: string) {
+		if (!id) {
+			return;
+		}
+		await this.isReady;
+		const data = await this.storage.getLocking();
+		if (!data || !(this.connectorId in data) || !data[this.connectorId]) {
+			this.storage.unlock();
+			return;
+		}
+		delete data[this.connectorId][id];
+		this.storage.setLocking(data);
+	}
+
+	/**
+	 * @param id - ID of channel to check
+	 *
+	 * @returns true if channel isn't blocklisted; false if it is
+	 */
+	public async shouldScrobbleChannel(
+		id: string | undefined | null,
+	): Promise<boolean> {
+		if (!id) {
+			return true;
+		}
+		await this.isReady;
+		const data = await this.storage.get();
+		if (!data || !(this.connectorId in data) || !data[this.connectorId]) {
+			return true;
+		}
+
+		return data[this.connectorId][id] !== true;
+	}
+}
diff --git a/src/core/storage/browser-storage.ts b/src/core/storage/browser-storage.ts
index 21de20c1..2572dc23 100644
--- a/src/core/storage/browser-storage.ts
+++ b/src/core/storage/browser-storage.ts
@@ -62,6 +62,12 @@ export const NOTIFICATIONS = 'Notifications';
  */
 export const LOCAL_CACHE = 'LocalCache';
 
+/**
+ * This storage contains the blocklist of each connector.
+ * Each blocklist is an object with channel IDs as keys.
+ */
+export const BLOCKLISTS = 'Blocklists';
+
 /**
  * This storage contains the options values.
  * @see DEFAULT_OPTIONS_MAP object in `config` module.
@@ -127,7 +133,9 @@ const storageTypeMap = {
 	ListenBrainz: LOCAL,
 	Maloja: LOCAL,
 	Webhook: LOCAL,
+	Pleroma: LOCAL,
 
+	[BLOCKLISTS]: LOCAL,
 	[LOCAL_CACHE]: LOCAL,
 	[REGEX_EDITS]: LOCAL,
 	[CORE]: LOCAL,
diff --git a/src/core/storage/wrapper.ts b/src/core/storage/wrapper.ts
index 44585f3d..8eb526ed 100644
--- a/src/core/storage/wrapper.ts
+++ b/src/core/storage/wrapper.ts
@@ -12,6 +12,7 @@ import {
 	REGEX_EDITS,
 	STATE_MANAGEMENT,
 	StorageNamespace,
+	BLOCKLISTS,
 } from '@/core/storage/browser-storage';
 import {
 	ConnectorOptions,
@@ -70,6 +71,7 @@ export interface ScrobblerModels {
 	ListenBrainz?: ListenBrainzModel;
 	Maloja?: Properties;
 	Webhook?: WebhookModel;
+	Pleroma?: Properties;
 }
 
 export interface ManagerTab {
@@ -83,6 +85,10 @@ export interface StateManagement {
 	browserPreferredTheme: 'light' | 'dark';
 }
 
+export type Blocklists = Record<string, Blocklist>;
+
+export type Blocklist = Record<string, true>;
+
 export interface DataModels extends ScrobblerModels {
 	/* sync options */
 	[CUSTOM_PATTERNS]: CustomPatterns;
@@ -95,6 +101,7 @@ export interface DataModels extends ScrobblerModels {
 	[CORE]: { appVersion: string };
 	[LOCAL_CACHE]: { [key: string]: SavedEdit };
 	[REGEX_EDITS]: RegexEdit[];
+	[BLOCKLISTS]: Blocklists;
 
 	/* state management */
 	[STATE_MANAGEMENT]: StateManagement;
diff --git a/src/ui/options/components/accounts.tsx b/src/ui/options/components/accounts.tsx
index 626210c4..97b7616d 100644
--- a/src/ui/options/components/accounts.tsx
+++ b/src/ui/options/components/accounts.tsx
@@ -51,6 +51,18 @@ const scrobblerPropertiesMap = {
 			placeholder: 'accountsUserTokenPlaceholder',
 		},
 	},
+	Pleroma: {
+		userApiUrl: {
+			type: 'text',
+			title: 'instanceDomain',
+			placeholder: 'instanceDomainPlaceholder',
+		},
+		userToken: {
+			type: 'password',
+			title: 'accountsUserToken',
+			placeholder: 'accountsUserTokenPlaceholder',
+		},
+	},
 };
 
 /**
@@ -83,6 +95,7 @@ export default function Accounts() {
 			<ScrobblerDisplay label="ListenBrainz" />
 			<ScrobblerDisplay label="Maloja" />
 			<ScrobblerDisplay label="Webhook" />
+			<ScrobblerDisplay label="Pleroma" />
 		</>
 	);
 }
diff --git a/src/ui/options/components/connector-override.tsx b/src/ui/options/components/connector-override.tsx
index 9abd2ef8..1c3392d9 100644
--- a/src/ui/options/components/connector-override.tsx
+++ b/src/ui/options/components/connector-override.tsx
@@ -3,6 +3,7 @@ import * as Options from '@/core/storage/options';
 import {
 	Accessor,
 	For,
+	Setter,
 	Show,
 	Suspense,
 	createEffect,
@@ -23,6 +24,8 @@ import {
 } from './inputs';
 import browser from 'webextension-polyfill';
 import { t } from '@/util/i18n';
+import BlockedChannels from './edit-options/blocked-channels';
+import { ModalType } from './navigator';
 
 const globalOptions = BrowserStorage.getStorage(BrowserStorage.OPTIONS);
 const connectorOverrideOptions = BrowserStorage.getStorage(
@@ -45,7 +48,10 @@ const [customPatternOptions, setCustomPatternOptions] = createResource(
 /**
  * Component that shows the override options for all connectors
  */
-export default function ConnectorOverrideOptions() {
+export default function ConnectorOverrideOptions(props: {
+	setActiveModal: Setter<ModalType>;
+	modal: HTMLDialogElement | undefined;
+}) {
 	return (
 		<>
 			<h1>{t('optionsSupportedWebsites')}</h1>
@@ -95,7 +101,10 @@ export default function ConnectorOverrideOptions() {
 					/>
 				</li>
 
-				<ConnectorOptions />
+				<ConnectorOptions
+					setActiveModal={props.setActiveModal}
+					modal={props.modal}
+				/>
 			</ul>
 		</>
 	);
@@ -104,11 +113,20 @@ export default function ConnectorOverrideOptions() {
 /**
  * Connector Override Options list
  */
-function ConnectorOptions() {
+function ConnectorOptions(props: {
+	setActiveModal: Setter<ModalType>;
+	modal: HTMLDialogElement | undefined;
+}) {
 	return (
 		<Suspense fallback={<p>{t('optionsLoadingConnectorOptions')}</p>}>
 			<For each={connectors}>
-				{(connector) => <ConnectorOption connector={connector} />}
+				{(connector) => (
+					<ConnectorOption
+						setActiveModal={props.setActiveModal}
+						modal={props.modal}
+						connector={connector}
+					/>
+				)}
 			</For>
 		</Suspense>
 	);
@@ -117,7 +135,11 @@ function ConnectorOptions() {
 /**
  * The connector override options for one connector
  */
-function ConnectorOption(props: { connector: ConnectorMeta }) {
+function ConnectorOption(props: {
+	connector: ConnectorMeta;
+	setActiveModal: Setter<ModalType>;
+	modal: HTMLDialogElement | undefined;
+}) {
 	const [ref, setRef] = createSignal<HTMLDetailsElement>();
 	const [active, setActive] = createSignal(false);
 	createEffect(() => {
@@ -167,6 +189,8 @@ function ConnectorOption(props: { connector: ConnectorMeta }) {
 					/>
 				</summary>
 				<ConnectorOverrideOptionDetails
+					setActiveModal={props.setActiveModal}
+					modal={props.modal}
 					connector={props.connector}
 					active={active}
 				/>
@@ -181,6 +205,8 @@ function ConnectorOption(props: { connector: ConnectorMeta }) {
 function ConnectorOverrideOptionDetails(props: {
 	connector: ConnectorMeta;
 	active: Accessor<boolean>;
+	setActiveModal: Setter<ModalType>;
+	modal: HTMLDialogElement | undefined;
 }) {
 	return (
 		<Show when={props.active()}>
@@ -313,6 +339,13 @@ function ConnectorOverrideOptionDetails(props: {
 			<h3>{t('customPatterns')}</h3>
 			<p>{t('customPatternsHint')}</p>
 			<EditCustomPatterns connector={props.connector} />
+			<Show when={props.connector.usesBlocklist}>
+				<BlockedChannels
+					setActiveModal={props.setActiveModal}
+					modal={props.modal}
+					connector={props.connector}
+				/>
+			</Show>
 		</Show>
 	);
 }
diff --git a/src/ui/options/components/edit-options/blocked-channels.tsx b/src/ui/options/components/edit-options/blocked-channels.tsx
new file mode 100644
index 00000000..4b08b3ca
--- /dev/null
+++ b/src/ui/options/components/edit-options/blocked-channels.tsx
@@ -0,0 +1,127 @@
+import { t } from '@/util/i18n';
+import {
+	For,
+	Setter,
+	createEffect,
+	createResource,
+	createSignal,
+} from 'solid-js';
+import * as BrowserStorage from '@/core/storage/browser-storage';
+import styles from '../components.module.scss';
+import Delete from '@suid/icons-material/DeleteOutlined';
+import { ExportBlocklist, ImportBlocklist, ViewBlocklist } from './util';
+import { ModalType } from '../navigator';
+import { ConnectorMeta } from '@/core/connectors';
+import { Blocklists } from '@/core/storage/wrapper';
+
+const blocklistStorage = BrowserStorage.getStorage(BrowserStorage.BLOCKLISTS);
+const [blocklists, { mutate }] = createResource(
+	blocklistStorage.get.bind(blocklistStorage),
+);
+const [connector, setConnector] = createSignal<ConnectorMeta>();
+
+/**
+ * Component that allows the user to see, import, and export connector blocklists.
+ */
+export default function BlockedChannels(props: {
+	setActiveModal: Setter<ModalType>;
+	modal: HTMLDialogElement | undefined;
+	connector: ConnectorMeta;
+}) {
+	createEffect(() => {
+		setConnector(props.connector);
+	});
+
+	return (
+		<>
+			<h3>{t('optionsBlockedSources')}</h3>
+			<p>{t('optionsBlockedSourcesDesc')}</p>
+			<div class={styles.buttonContainer}>
+				<ViewBlocklist
+					setActiveModal={props.setActiveModal}
+					modal={props.modal}
+					type={'blocklist'}
+				/>
+				<ExportBlocklist
+					blocklistWrapper={blocklistStorage}
+					connector={props.connector}
+					filename={`${props.connector.id}-blocklist.json`}
+				/>
+				<ImportBlocklist
+					blocklistWrapper={blocklistStorage}
+					connector={props.connector}
+					mutate={mutate}
+				/>
+			</div>
+		</>
+	);
+}
+
+/**
+ * Component that shows all the currently registered blocked channels and allows the user to unblock them.
+ * To be displayed in a modal.
+ */
+export function BlocklistModal() {
+	return (
+		<>
+			<h1>
+				{t(
+					'optionsBlockedSourcesPopupTitle',
+					Object.keys(
+						blocklists()?.[connector()?.id ?? ''] ?? {},
+					).length.toString(),
+				)}
+			</h1>
+			<ul class={styles.optionList}>
+				<For
+					each={Object.entries(
+						blocklists()?.[connector()?.id ?? ''] ?? {},
+					)}
+				>
+					{([channel]) => (
+						<ChannelInfo
+							channel={channel}
+							connector={connector()}
+							mutate={mutate}
+						/>
+					)}
+				</For>
+			</ul>
+		</>
+	);
+}
+
+/**
+ * Component that shows a blocked channel and allows the user to delete it.
+ */
+function ChannelInfo(props: {
+	channel: string;
+	connector: ConnectorMeta | undefined;
+	mutate: Setter<Blocklists | null | undefined>;
+}) {
+	return (
+		<li class={styles.deleteListing}>
+			<button
+				class={`${styles.button} ${styles.small} ${styles.marginRight}`}
+				onClick={(event) => {
+					event.stopPropagation();
+					const channel = props.channel;
+					const connector = props.connector;
+					props.mutate((e) => {
+						if (!e || !connector || !e[connector.id]) {
+							return e;
+						}
+						delete e[connector.id][channel];
+						blocklistStorage.set(e);
+						return {
+							...e,
+						};
+					});
+				}}
+			>
+				<Delete />
+			</button>
+			<span>{props.channel}</span>
+		</li>
+	);
+}
diff --git a/src/ui/options/components/edit-options/util.tsx b/src/ui/options/components/edit-options/util.tsx
index 5bb953dc..39f230a4 100644
--- a/src/ui/options/components/edit-options/util.tsx
+++ b/src/ui/options/components/edit-options/util.tsx
@@ -1,4 +1,4 @@
-import StorageWrapper from '@/core/storage/wrapper';
+import StorageWrapper, { Blocklists } from '@/core/storage/wrapper';
 import * as Options from '@/core/storage/options';
 import * as BrowserStorage from '@/core/storage/browser-storage';
 import Visibility from '@suid/icons-material/VisibilityOutlined';
@@ -9,6 +9,7 @@ import { t } from '@/util/i18n';
 import { Setter, createSignal } from 'solid-js';
 import { RegexEdit } from '@/util/regex';
 import { ModalType } from '../navigator';
+import { ConnectorMeta } from '@/core/connectors';
 
 type EditWrapper = StorageWrapper<
 	typeof BrowserStorage.REGEX_EDITS | typeof BrowserStorage.LOCAL_CACHE
@@ -45,6 +46,29 @@ export function ViewEdits(props: {
 	);
 }
 
+/**
+ * Button that allows the user to open the modal that shows them their track edits
+ */
+export function ViewBlocklist(props: {
+	setActiveModal: Setter<ModalType>;
+	modal: HTMLDialogElement | undefined;
+	type: ModalType;
+}) {
+	return (
+		<button
+			class={`${styles.button} ${styles.shiftLeft}`}
+			onClick={(e) => {
+				e.stopImmediatePropagation();
+				props.setActiveModal(props.type);
+				props.modal?.showModal();
+			}}
+		>
+			<Visibility />
+			{t('optionsViewEdited')}
+		</button>
+	);
+}
+
 /**
  * Button that exports edits for the user
  */
@@ -65,6 +89,52 @@ export function ExportEdits(props: {
 	);
 }
 
+/**
+ * Button that exports blocklist for the user
+ */
+export function ExportBlocklist(props: {
+	blocklistWrapper: StorageWrapper<typeof BrowserStorage.BLOCKLISTS>;
+	connector: ConnectorMeta;
+	filename: string;
+}) {
+	return (
+		<button
+			class={`${styles.button} ${styles.shiftLeft}`}
+			onClick={() =>
+				void downloadBlocklist(
+					props.blocklistWrapper,
+					props.connector,
+					props.filename,
+				)
+			}
+		>
+			<Upload />
+			{t('optionsExportEdited')}
+		</button>
+	);
+}
+
+/**
+ * Compiles all the users track edits from storage and downloads them
+ */
+async function downloadBlocklist(
+	blocklistWrapper: StorageWrapper<typeof BrowserStorage.BLOCKLISTS>,
+	connector: ConnectorMeta,
+	filename: string,
+) {
+	const blocklists = await blocklistWrapper.get();
+	if (!blocklists || !blocklists[connector.id]) {
+		return;
+	}
+	const data = `data:text/json;charset=UTF-8,${encodeURIComponent(
+		JSON.stringify(blocklists[connector.id]),
+	)}`;
+	const a = document.createElement('a');
+	a.download = filename;
+	a.href = data;
+	a.click();
+}
+
 /**
  * Compiles all the users track edits from storage and downloads them
  */
@@ -82,6 +152,40 @@ async function downloadEdits(editWrapper: EditWrapper, filename: string) {
 	a.click();
 }
 
+/**
+ * Button that allows the user to upload a .json file and get blocklist from it.
+ */
+export function ImportBlocklist(props: {
+	blocklistWrapper: StorageWrapper<typeof BrowserStorage.BLOCKLISTS>;
+	connector: ConnectorMeta;
+	mutate: Setter<Blocklists | null | undefined>;
+}) {
+	const [ref, setRef] = createSignal<HTMLInputElement>();
+	return (
+		<button
+			class={`${styles.button} ${styles.shiftLeft}`}
+			onClick={() => ref()?.click()}
+		>
+			<Download />
+			{t('optionsImportEdited')}
+			<input
+				hidden={true}
+				ref={setRef}
+				type="file"
+				accept=".json"
+				onChange={(e) =>
+					pushBlocklist(
+						e,
+						props.blocklistWrapper,
+						props.connector,
+						props.mutate,
+					)
+				}
+			/>
+		</button>
+	);
+}
+
 /**
  * Button that allows the user to upload a .json file and get edits from it.
  */
@@ -138,10 +242,52 @@ function pushEdits(
 						: {
 								...oldEdits,
 								...edits,
-						  };
+							};
 				editWrapper.set(newEdits);
 				mutate(newEdits);
 			})(arg),
 	);
 	reader.readAsText(file);
 }
+
+/**
+ * Reads an imported .json file and import the blocklist to storage
+ */
+function pushBlocklist(
+	e: Event & {
+		currentTarget: HTMLInputElement;
+		target: Element;
+	},
+	blocklistWrapper: StorageWrapper<typeof BrowserStorage.BLOCKLISTS>,
+	connector: ConnectorMeta,
+	mutate: Setter<Blocklists | null | undefined>,
+) {
+	const file = e.currentTarget.files?.[0];
+	if (!file) {
+		return;
+	}
+	const reader = new FileReader();
+	reader.addEventListener(
+		'load',
+		(arg) =>
+			void (async (e) => {
+				const newBlocklist = JSON.parse(
+					e.target?.result as string,
+				) as Record<string, true>;
+				let blocklists = await blocklistWrapper.get();
+				if (!blocklists) {
+					blocklists = {};
+				}
+				if (!blocklists[connector.id]) {
+					blocklists[connector.id] = {};
+				}
+				blocklists[connector.id] = {
+					...blocklists[connector.id],
+					...newBlocklist,
+				};
+				blocklistWrapper.set(blocklists);
+				mutate(blocklists);
+			})(arg),
+	);
+	reader.readAsText(file);
+}
diff --git a/src/ui/options/components/navigator.ts b/src/ui/options/components/navigator.ts
index 83f8924b..64b3af26 100644
--- a/src/ui/options/components/navigator.ts
+++ b/src/ui/options/components/navigator.ts
@@ -32,7 +32,7 @@ import AdvancedOptionsComponent from './advanced-settings';
 /**
  * Type indicating possible states for modal
  */
-export type ModalType = 'savedEdits' | 'regexEdits' | '';
+export type ModalType = 'savedEdits' | 'regexEdits' | 'blocklist' | '';
 
 /**
  * Singular navigator button
diff --git a/src/ui/options/main.tsx b/src/ui/options/main.tsx
index f11d64a1..e457e70a 100644
--- a/src/ui/options/main.tsx
+++ b/src/ui/options/main.tsx
@@ -1,8 +1,8 @@
-import { render } from 'solid-js/web';
+import { Dynamic, render } from 'solid-js/web';
 import styles from './settings.module.scss';
 import { initializeThemes } from '@/theme/themes';
 import '@/theme/themes.scss';
-import { Match, Show, Switch, createSignal, onCleanup } from 'solid-js';
+import { Show, createSignal, onCleanup } from 'solid-js';
 import Close from '@suid/icons-material/CloseOutlined';
 import Sidebar from './sidebar/sidebar';
 import { EditsModal } from './components/edit-options/edited-tracks';
@@ -18,6 +18,7 @@ import {
 	showSomeLoveItem,
 } from './components/navigator';
 import ContextMenu from '../components/context-menu/context-menu';
+import { BlocklistModal } from './components/edit-options/blocked-channels';
 
 /**
  * Media query for detecting whether to use context menu or sidebar
@@ -49,6 +50,13 @@ function getDefaultSetting(): NavigatorNavigationButton {
 
 const defaultSetting = getDefaultSetting();
 
+const modalMap = {
+	savedEdits: EditsModal,
+	regexEdits: RegexEditsModal,
+	blocklist: BlocklistModal,
+	'': () => <div>Loading...</div>,
+};
+
 /**
  * Preferences component, with a sidebar and several different options and info pages
  */
@@ -75,9 +83,8 @@ function Options() {
 	document.addEventListener('click', onclick);
 	onCleanup(() => document.removeEventListener('click', onclick));
 
-	const [shouldShowContextMenu, setShouldShowContextMenu] = createSignal(
-		contextMenuQuery(),
-	);
+	const [shouldShowContextMenu, setShouldShowContextMenu] =
+		createSignal(contextMenuQuery());
 	const resizeListener = () => setShouldShowContextMenu(contextMenuQuery());
 	window.addEventListener('resize', resizeListener);
 
@@ -112,14 +119,7 @@ function Options() {
 				onClose={() => setActiveModal('')}
 			>
 				<div class={styles.modalContent}>
-					<Switch fallback={<div>Loading...</div>}>
-						<Match when={activeModal() === 'savedEdits'}>
-							<EditsModal />
-						</Match>
-						<Match when={activeModal() === 'regexEdits'}>
-							<RegexEditsModal />
-						</Match>
-					</Switch>
+					<Dynamic component={modalMap[activeModal()]} />
 				</div>
 				<button
 					class={styles.modalClose}
diff --git a/src/util/communication.ts b/src/util/communication.ts
index 9f42945e..b83b661a 100644
--- a/src/util/communication.ts
+++ b/src/util/communication.ts
@@ -156,6 +156,21 @@ interface BackgroundCommunications {
 		payload: undefined;
 		response: void;
 	};
+	addToBlocklist: {
+		payload: undefined;
+		response: Promise<void>;
+	};
+	removeFromBlocklist: {
+		payload: undefined;
+		response: Promise<void>;
+	};
+	getChannelDetails: {
+		payload: undefined;
+		response: {
+			connector: ConnectorMeta;
+			channelId: string | null | undefined;
+		};
+	};
 }
 
 /**
diff --git a/src/util/util.ts b/src/util/util.ts
index 22905884..ad590122 100644
--- a/src/util/util.ts
+++ b/src/util/util.ts
@@ -10,6 +10,7 @@ import {
 	WebhookModel,
 	Properties,
 	StateManagement,
+	Blocklists,
 } from '@/core/storage/wrapper';
 import { RegexEdit } from './regex';
 
@@ -137,7 +138,8 @@ export function hideObjectValue(
 		| ListenBrainzModel
 		| WebhookModel
 		| StateManagement
-		| RegexEdit[],
+		| RegexEdit[]
+		| Blocklists,
 ): string {
 	if (!keyValue) {
 		if (keyValue === null) {


Test patch:
diff --git a/tests/background/connectors.test.ts b/tests/background/connectors.test.ts
index 5b22f4b6..e9374038 100644
--- a/tests/background/connectors.test.ts
+++ b/tests/background/connectors.test.ts
@@ -7,12 +7,14 @@ import connectors, { ConnectorMeta } from '@/core/connectors';
 import * as UrlMatch from '@/util/url-match';
 
 const PROP_TYPES: {
+	usesBlocklist: 'boolean';
 	allFrames: 'boolean';
 	matches: 'array';
 	label: 'string';
 	js: 'string';
 	id: 'string';
 } = {
+	usesBlocklist: 'boolean',
 	allFrames: 'boolean',
 	matches: 'array',
 	label: 'string',