Repository: web-scrobbler/web-scrobbler
Pull request #4001 (2023-07-30 12:46:33)

Add TrackerHub scrobble support
**Describe the changes you made**
Adds support for https://trackerhub.vercel.app (dynamic music website)

**Additional context**
it uses the mediasession api with action handlers
![image](https://github.com/web-scrobbler/web-scrobbler/assets/68407783/f6e1f77c-9f07-4a49-9570-2b3982019145)
and heres the player element
![image](https://github.com/web-scrobbler/web-scrobbler/assets/68407783/3819694a-8362-43a3-a78c-9d261161a866)
tested ✅
![image](https://github.com/web-scrobbler/web-scrobbler/assets/68407783/9b2e508f-d2d1-46cd-9e36-757c93ec38d1)




Patch:
diff --git a/package-lock.json b/package-lock.json
index f04e5cb4..27b74aac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,11 +9,11 @@
       "version": "3.0.15",
       "license": "MIT",
       "dependencies": {
-        "@suid/icons-material": "0.6.6",
+        "@suid/icons-material": "0.6.9",
         "@web-scrobbler/metadata-filter": "3.0.1",
         "blueimp-md5": "2.19.0",
         "stylelint": "13.13.1",
-        "vite": "4.4.7",
+        "vite": "4.4.8",
         "webextension-polyfill": "0.10.0"
       },
       "devDependencies": {
@@ -22,17 +22,17 @@
         "@types/fs-extra": "11.0.1",
         "@types/imagemin": "8.0.1",
         "@types/imagemin-jpegtran": "5.0.1",
-        "@types/node": "20.4.4",
-        "@types/react": "18.2.15",
+        "@types/node": "20.4.8",
+        "@types/react": "18.2.18",
         "@types/webextension-polyfill": "0.10.1",
         "@typescript-eslint/eslint-plugin": "5.60.1",
         "archiver": "5.3.1",
         "canvas": "2.11.2",
         "chokidar": "3.5.3",
-        "esbuild": "0.18.15",
-        "eslint": "8.45.0",
+        "esbuild": "0.18.17",
+        "eslint": "8.46.0",
         "eslint-config-web-scrobbler": "2.1.1",
-        "eslint-plugin-react": "7.33.0",
+        "eslint-plugin-react": "7.33.1",
         "eslint-plugin-solid": "0.12.1",
         "eslint-plugin-tsdoc": "0.2.17",
         "fs-extra": "11.1.1",
@@ -40,20 +40,20 @@
         "imagemin": "8.0.1",
         "imagemin-jpegtran": "7.0.0",
         "imagemin-pngquant": "9.0.2",
-        "prettier": "3.0.0",
+        "prettier": "3.0.1",
         "prettier-config-web-scrobbler": "0.1.0",
         "remark": "14.0.3",
         "remark-cli": "11.0.0",
         "remark-preset-lint-recommended": "6.1.3",
-        "sass": "1.64.1",
-        "solid-js": "1.7.8",
+        "sass": "1.64.2",
+        "solid-js": "1.7.9",
         "stylelint-config-web-scrobbler": "0.2.0",
         "tsx": "3.12.7",
         "typescript": "5.1.6",
         "vite-plugin-conditional-compiler": "0.1.1",
         "vite-plugin-solid": "2.7.0",
         "vite-plugin-static-copy": "0.17.0",
-        "vitest": "0.33.0"
+        "vitest": "0.34.1"
       },
       "engines": {
         "node": ">=18.0.0"
@@ -1073,9 +1073,9 @@
       }
     },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.15.tgz",
-      "integrity": "sha512-wlkQBWb79/jeEEoRmrxt/yhn5T1lU236OCNpnfRzaCJHZ/5gf82uYx1qmADTBWE0AR/v7FiozE1auk2riyQd3w==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.17.tgz",
+      "integrity": "sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==",
       "cpu": [
         "arm"
       ],
@@ -1088,9 +1088,9 @@
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.15.tgz",
-      "integrity": "sha512-NI/gnWcMl2kXt1HJKOn2H69SYn4YNheKo6NZt1hyfKWdMbaGadxjZIkcj4Gjk/WPxnbFXs9/3HjGHaknCqjrww==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.17.tgz",
+      "integrity": "sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==",
       "cpu": [
         "arm64"
       ],
@@ -1103,9 +1103,9 @@
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.15.tgz",
-      "integrity": "sha512-FM9NQamSaEm/IZIhegF76aiLnng1kEsZl2eve/emxDeReVfRuRNmvT28l6hoFD9TsCxpK+i4v8LPpEj74T7yjA==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.17.tgz",
+      "integrity": "sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==",
       "cpu": [
         "x64"
       ],
@@ -1118,9 +1118,9 @@
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.15.tgz",
-      "integrity": "sha512-XmrFwEOYauKte9QjS6hz60FpOCnw4zaPAb7XV7O4lx1r39XjJhTN7ZpXqJh4sN6q60zbP6QwAVVA8N/wUyBH/w==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.17.tgz",
+      "integrity": "sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==",
       "cpu": [
         "arm64"
       ],
@@ -1133,9 +1133,9 @@
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.15.tgz",
-      "integrity": "sha512-bMqBmpw1e//7Fh5GLetSZaeo9zSC4/CMtrVFdj+bqKPGJuKyfNJ5Nf2m3LknKZTS+Q4oyPiON+v3eaJ59sLB5A==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.17.tgz",
+      "integrity": "sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==",
       "cpu": [
         "x64"
       ],
@@ -1148,9 +1148,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.15.tgz",
-      "integrity": "sha512-LoTK5N3bOmNI9zVLCeTgnk5Rk0WdUTrr9dyDAQGVMrNTh9EAPuNwSTCgaKOKiDpverOa0htPcO9NwslSE5xuLA==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.17.tgz",
+      "integrity": "sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==",
       "cpu": [
         "arm64"
       ],
@@ -1163,9 +1163,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.15.tgz",
-      "integrity": "sha512-62jX5n30VzgrjAjOk5orYeHFq6sqjvsIj1QesXvn5OZtdt5Gdj0vUNJy9NIpjfdNdqr76jjtzBJKf+h2uzYuTQ==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.17.tgz",
+      "integrity": "sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==",
       "cpu": [
         "x64"
       ],
@@ -1178,9 +1178,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.15.tgz",
-      "integrity": "sha512-dT4URUv6ir45ZkBqhwZwyFV6cH61k8MttIwhThp2BGiVtagYvCToF+Bggyx2VI57RG4Fbt21f9TmXaYx0DeUJg==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.17.tgz",
+      "integrity": "sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==",
       "cpu": [
         "arm"
       ],
@@ -1193,9 +1193,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.15.tgz",
-      "integrity": "sha512-BWncQeuWDgYv0jTNzJjaNgleduV4tMbQjmk/zpPh/lUdMcNEAxy+jvneDJ6RJkrqloG7tB9S9rCrtfk/kuplsQ==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.17.tgz",
+      "integrity": "sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==",
       "cpu": [
         "arm64"
       ],
@@ -1208,9 +1208,9 @@
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.15.tgz",
-      "integrity": "sha512-JPXORvgHRHITqfms1dWT/GbEY89u848dC08o0yK3fNskhp0t2TuNUnsrrSgOdH28ceb1hJuwyr8R/1RnyPwocw==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.17.tgz",
+      "integrity": "sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==",
       "cpu": [
         "ia32"
       ],
@@ -1223,9 +1223,9 @@
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.15.tgz",
-      "integrity": "sha512-kArPI0DopjJCEplsVj/H+2Qgzz7vdFSacHNsgoAKpPS6W/Ndh8Oe24HRDQ5QCu4jHgN6XOtfFfLpRx3TXv/mEg==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.17.tgz",
+      "integrity": "sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==",
       "cpu": [
         "loong64"
       ],
@@ -1238,9 +1238,9 @@
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.15.tgz",
-      "integrity": "sha512-b/tmngUfO02E00c1XnNTw/0DmloKjb6XQeqxaYuzGwHe0fHVgx5/D6CWi+XH1DvkszjBUkK9BX7n1ARTOst59w==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.17.tgz",
+      "integrity": "sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==",
       "cpu": [
         "mips64el"
       ],
@@ -1253,9 +1253,9 @@
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.15.tgz",
-      "integrity": "sha512-KXPY69MWw79QJkyvUYb2ex/OgnN/8N/Aw5UDPlgoRtoEfcBqfeLodPr42UojV3NdkoO4u10NXQdamWm1YEzSKw==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.17.tgz",
+      "integrity": "sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==",
       "cpu": [
         "ppc64"
       ],
@@ -1268,9 +1268,9 @@
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.15.tgz",
-      "integrity": "sha512-komK3NEAeeGRnvFEjX1SfVg6EmkfIi5aKzevdvJqMydYr9N+pRQK0PGJXk+bhoPZwOUgLO4l99FZmLGk/L1jWg==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.17.tgz",
+      "integrity": "sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==",
       "cpu": [
         "riscv64"
       ],
@@ -1283,9 +1283,9 @@
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.15.tgz",
-      "integrity": "sha512-632T5Ts6gQ2WiMLWRRyeflPAm44u2E/s/TJvn+BP6M5mnHSk93cieaypj3VSMYO2ePTCRqAFXtuYi1yv8uZJNA==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.17.tgz",
+      "integrity": "sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==",
       "cpu": [
         "s390x"
       ],
@@ -1298,9 +1298,9 @@
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.15.tgz",
-      "integrity": "sha512-MsHtX0NgvRHsoOtYkuxyk4Vkmvk3PLRWfA4okK7c+6dT0Fu4SUqXAr9y4Q3d8vUf1VWWb6YutpL4XNe400iQ1g==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz",
+      "integrity": "sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==",
       "cpu": [
         "x64"
       ],
@@ -1313,9 +1313,9 @@
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.15.tgz",
-      "integrity": "sha512-djST6s+jQiwxMIVQ5rlt24JFIAr4uwUnzceuFL7BQT4CbrRtqBPueS4GjXSiIpmwVri1Icj/9pFRJ7/aScvT+A==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.17.tgz",
+      "integrity": "sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==",
       "cpu": [
         "x64"
       ],
@@ -1328,9 +1328,9 @@
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.15.tgz",
-      "integrity": "sha512-naeRhUIvhsgeounjkF5mvrNAVMGAm6EJWiabskeE5yOeBbLp7T89tAEw0j5Jm/CZAwyLe3c67zyCWH6fsBLCpw==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.17.tgz",
+      "integrity": "sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==",
       "cpu": [
         "x64"
       ],
@@ -1343,9 +1343,9 @@
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.15.tgz",
-      "integrity": "sha512-qkT2+WxyKbNIKV1AEhI8QiSIgTHMcRctzSaa/I3kVgMS5dl3fOeoqkb7pW76KwxHoriImhx7Mg3TwN/auMDsyQ==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.17.tgz",
+      "integrity": "sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==",
       "cpu": [
         "x64"
       ],
@@ -1358,9 +1358,9 @@
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.15.tgz",
-      "integrity": "sha512-HC4/feP+pB2Vb+cMPUjAnFyERs+HJN7E6KaeBlFdBv799MhD+aPJlfi/yk36SED58J9TPwI8MAcVpJgej4ud0A==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.17.tgz",
+      "integrity": "sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==",
       "cpu": [
         "arm64"
       ],
@@ -1373,9 +1373,9 @@
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.15.tgz",
-      "integrity": "sha512-ovjwoRXI+gf52EVF60u9sSDj7myPixPxqzD5CmkEUmvs+W9Xd0iqISVBQn8xcx4ciIaIVlWCuTbYDOXOnOL44Q==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.17.tgz",
+      "integrity": "sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==",
       "cpu": [
         "ia32"
       ],
@@ -1388,9 +1388,9 @@
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.15.tgz",
-      "integrity": "sha512-imUxH9a3WJARyAvrG7srLyiK73XdX83NXQkjKvQ+7vPh3ZxoLrzvPkQKKw2DwZ+RV2ZB6vBfNHP8XScAmQC3aA==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.17.tgz",
+      "integrity": "sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==",
       "cpu": [
         "x64"
       ],
@@ -1418,18 +1418,18 @@
       }
     },
     "node_modules/@eslint-community/regexpp": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz",
-      "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==",
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz",
+      "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==",
       "dev": true,
       "engines": {
         "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
       }
     },
     "node_modules/@eslint/eslintrc": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz",
-      "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz",
+      "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==",
       "dev": true,
       "dependencies": {
         "ajv": "^6.12.4",
@@ -1450,9 +1450,9 @@
       }
     },
     "node_modules/@eslint/js": {
-      "version": "8.44.0",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz",
-      "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==",
+      "version": "8.46.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz",
+      "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -2064,20 +2064,20 @@
       "integrity": "sha512-yzHAlf1CVi7n0SvUrMgs8Z49UiS9669+td1w1frekhRQuRbkXhHoyJkvovaDVJlWRmCPA8Q0f1OTr0uDCUg9mQ=="
     },
     "node_modules/@suid/icons-material": {
-      "version": "0.6.6",
-      "resolved": "https://registry.npmjs.org/@suid/icons-material/-/icons-material-0.6.6.tgz",
-      "integrity": "sha512-fyAU54qMsDX49AsTksS44SAfQK1FiGCcYKHUuHYCEmHD1BuFni29Kw4FnrSlf3cVbvm/vBRSlubdeWTiHcA/Og==",
+      "version": "0.6.9",
+      "resolved": "https://registry.npmjs.org/@suid/icons-material/-/icons-material-0.6.9.tgz",
+      "integrity": "sha512-qiKVwfhati4tDFPBLa0ad8R9xpXEgwFlec6PO1vBQVSnAEdIVh7j4I8pjbe1UyK91wi/p+deM1kBrhwOejS9Uw==",
       "dependencies": {
-        "@suid/material": "0.13.2"
+        "@suid/material": "0.14.2"
       },
       "peerDependencies": {
         "solid-js": "^1.7.7"
       }
     },
     "node_modules/@suid/material": {
-      "version": "0.13.2",
-      "resolved": "https://registry.npmjs.org/@suid/material/-/material-0.13.2.tgz",
-      "integrity": "sha512-VFPYNuyZ8exivyN5dpyDv5D7zrAmRl+bHHTowvZ1F7DPzxklBnGDyao30E0O7kqs07/zkQFoF41WixUwdOY/8A==",
+      "version": "0.14.2",
+      "resolved": "https://registry.npmjs.org/@suid/material/-/material-0.14.2.tgz",
+      "integrity": "sha512-VdYX4xC0aA+SL9DcSM1uz198Z+UJQiuvNZfK6IOyoDbdQatZJ1+zqpqUWeICdX/5krLq9rs7OBAxsjNNYkwIhA==",
       "dependencies": {
         "@suid/base": "0.8.4",
         "@suid/css": "0.4.0",
@@ -2325,9 +2325,9 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "20.4.4",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.4.tgz",
-      "integrity": "sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==",
+      "version": "20.4.8",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.8.tgz",
+      "integrity": "sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==",
       "devOptional": true
     },
     "node_modules/@types/normalize-package-data": {
@@ -2347,9 +2347,9 @@
       "dev": true
     },
     "node_modules/@types/react": {
-      "version": "18.2.15",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.15.tgz",
-      "integrity": "sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==",
+      "version": "18.2.18",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.18.tgz",
+      "integrity": "sha512-da4NTSeBv/P34xoZPhtcLkmZuJ+oYaCxHmyHzwaDQo9RQPBeXV+06gEk2FpqEcsX9XrnNLvRpVh6bdavDSjtiQ==",
       "dev": true,
       "dependencies": {
         "@types/prop-types": "*",
@@ -2753,13 +2753,13 @@
       }
     },
     "node_modules/@vitest/expect": {
-      "version": "0.33.0",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.33.0.tgz",
-      "integrity": "sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==",
+      "version": "0.34.1",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.1.tgz",
+      "integrity": "sha512-q2CD8+XIsQ+tHwypnoCk8Mnv5e6afLFvinVGCq3/BOT4kQdVQmY6rRfyKkwcg635lbliLPqbunXZr+L1ssUWiQ==",
       "dev": true,
       "dependencies": {
-        "@vitest/spy": "0.33.0",
-        "@vitest/utils": "0.33.0",
+        "@vitest/spy": "0.34.1",
+        "@vitest/utils": "0.34.1",
         "chai": "^4.3.7"
       },
       "funding": {
@@ -2767,12 +2767,12 @@
       }
     },
     "node_modules/@vitest/runner": {
-      "version": "0.33.0",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.33.0.tgz",
-      "integrity": "sha512-UPfACnmCB6HKRHTlcgCoBh6ppl6fDn+J/xR8dTufWiKt/74Y9bHci5CKB8tESSV82zKYtkBJo9whU3mNvfaisg==",
+      "version": "0.34.1",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.1.tgz",
+      "integrity": "sha512-YfQMpYzDsYB7yqgmlxZ06NI4LurHWfrH7Wy3Pvf/z/vwUSgq1zLAb1lWcItCzQG+NVox+VvzlKQrYEXb47645g==",
       "dev": true,
       "dependencies": {
-        "@vitest/utils": "0.33.0",
+        "@vitest/utils": "0.34.1",
         "p-limit": "^4.0.0",
         "pathe": "^1.1.1"
       },
@@ -2808,9 +2808,9 @@
       }
     },
     "node_modules/@vitest/snapshot": {
-      "version": "0.33.0",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.33.0.tgz",
-      "integrity": "sha512-tJjrl//qAHbyHajpFvr8Wsk8DIOODEebTu7pgBrP07iOepR5jYkLFiqLq2Ltxv+r0uptUb4izv1J8XBOwKkVYA==",
+      "version": "0.34.1",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.1.tgz",
+      "integrity": "sha512-0O9LfLU0114OqdF8lENlrLsnn024Tb1CsS9UwG0YMWY2oGTQfPtkW+B/7ieyv0X9R2Oijhi3caB1xgGgEgclSQ==",
       "dev": true,
       "dependencies": {
         "magic-string": "^0.30.1",
@@ -2822,9 +2822,9 @@
       }
     },
     "node_modules/@vitest/spy": {
-      "version": "0.33.0",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.33.0.tgz",
-      "integrity": "sha512-Kv+yZ4hnH1WdiAkPUQTpRxW8kGtH8VRTnus7ZTGovFYM1ZezJpvGtb9nPIjPnptHbsyIAxYZsEpVPYgtpjGnrg==",
+      "version": "0.34.1",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.1.tgz",
+      "integrity": "sha512-UT4WcI3EAPUNO8n6y9QoEqynGGEPmmRxC+cLzneFFXpmacivjHZsNbiKD88KUScv5DCHVDgdBsLD7O7s1enFcQ==",
       "dev": true,
       "dependencies": {
         "tinyspy": "^2.1.1"
@@ -2834,9 +2834,9 @@
       }
     },
     "node_modules/@vitest/utils": {
-      "version": "0.33.0",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.33.0.tgz",
-      "integrity": "sha512-pF1w22ic965sv+EN6uoePkAOTkAPWM03Ri/jXNyMIKBb/XHLDPfhLvf/Fa9g0YECevAIz56oVYXhodLvLQ/awA==",
+      "version": "0.34.1",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.1.tgz",
+      "integrity": "sha512-/ql9dsFi4iuEbiNcjNHQWXBum7aL8pyhxvfnD9gNtbjR9fUKAjxhj4AA3yfLXg6gJpMGGecvtF8Au2G9y3q47Q==",
       "dev": true,
       "dependencies": {
         "diff-sequences": "^29.4.3",
@@ -4874,9 +4874,9 @@
       }
     },
     "node_modules/esbuild": {
-      "version": "0.18.15",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.15.tgz",
-      "integrity": "sha512-3WOOLhrvuTGPRzQPU6waSDWrDTnQriia72McWcn6UCi43GhCHrXH4S59hKMeez+IITmdUuUyvbU9JIp+t3xlPQ==",
+      "version": "0.18.17",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.17.tgz",
+      "integrity": "sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==",
       "hasInstallScript": true,
       "bin": {
         "esbuild": "bin/esbuild"
@@ -4885,28 +4885,28 @@
         "node": ">=12"
       },
       "optionalDependencies": {
-        "@esbuild/android-arm": "0.18.15",
-        "@esbuild/android-arm64": "0.18.15",
-        "@esbuild/android-x64": "0.18.15",
-        "@esbuild/darwin-arm64": "0.18.15",
-        "@esbuild/darwin-x64": "0.18.15",
-        "@esbuild/freebsd-arm64": "0.18.15",
-        "@esbuild/freebsd-x64": "0.18.15",
-        "@esbuild/linux-arm": "0.18.15",
-        "@esbuild/linux-arm64": "0.18.15",
-        "@esbuild/linux-ia32": "0.18.15",
-        "@esbuild/linux-loong64": "0.18.15",
-        "@esbuild/linux-mips64el": "0.18.15",
-        "@esbuild/linux-ppc64": "0.18.15",
-        "@esbuild/linux-riscv64": "0.18.15",
-        "@esbuild/linux-s390x": "0.18.15",
-        "@esbuild/linux-x64": "0.18.15",
-        "@esbuild/netbsd-x64": "0.18.15",
-        "@esbuild/openbsd-x64": "0.18.15",
-        "@esbuild/sunos-x64": "0.18.15",
-        "@esbuild/win32-arm64": "0.18.15",
-        "@esbuild/win32-ia32": "0.18.15",
-        "@esbuild/win32-x64": "0.18.15"
+        "@esbuild/android-arm": "0.18.17",
+        "@esbuild/android-arm64": "0.18.17",
+        "@esbuild/android-x64": "0.18.17",
+        "@esbuild/darwin-arm64": "0.18.17",
+        "@esbuild/darwin-x64": "0.18.17",
+        "@esbuild/freebsd-arm64": "0.18.17",
+        "@esbuild/freebsd-x64": "0.18.17",
+        "@esbuild/linux-arm": "0.18.17",
+        "@esbuild/linux-arm64": "0.18.17",
+        "@esbuild/linux-ia32": "0.18.17",
+        "@esbuild/linux-loong64": "0.18.17",
+        "@esbuild/linux-mips64el": "0.18.17",
+        "@esbuild/linux-ppc64": "0.18.17",
+        "@esbuild/linux-riscv64": "0.18.17",
+        "@esbuild/linux-s390x": "0.18.17",
+        "@esbuild/linux-x64": "0.18.17",
+        "@esbuild/netbsd-x64": "0.18.17",
+        "@esbuild/openbsd-x64": "0.18.17",
+        "@esbuild/sunos-x64": "0.18.17",
+        "@esbuild/win32-arm64": "0.18.17",
+        "@esbuild/win32-ia32": "0.18.17",
+        "@esbuild/win32-x64": "0.18.17"
       }
     },
     "node_modules/escalade": {
@@ -4930,27 +4930,27 @@
       }
     },
     "node_modules/eslint": {
-      "version": "8.45.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz",
-      "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==",
+      "version": "8.46.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz",
+      "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
-        "@eslint-community/regexpp": "^4.4.0",
-        "@eslint/eslintrc": "^2.1.0",
-        "@eslint/js": "8.44.0",
+        "@eslint-community/regexpp": "^4.6.1",
+        "@eslint/eslintrc": "^2.1.1",
+        "@eslint/js": "^8.46.0",
         "@humanwhocodes/config-array": "^0.11.10",
         "@humanwhocodes/module-importer": "^1.0.1",
         "@nodelib/fs.walk": "^1.2.8",
-        "ajv": "^6.10.0",
+        "ajv": "^6.12.4",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
         "debug": "^4.3.2",
         "doctrine": "^3.0.0",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^7.2.0",
-        "eslint-visitor-keys": "^3.4.1",
-        "espree": "^9.6.0",
+        "eslint-scope": "^7.2.2",
+        "eslint-visitor-keys": "^3.4.2",
+        "espree": "^9.6.1",
         "esquery": "^1.4.2",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
@@ -5023,9 +5023,9 @@
       }
     },
     "node_modules/eslint-plugin-react": {
-      "version": "7.33.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.0.tgz",
-      "integrity": "sha512-qewL/8P34WkY8jAqdQxsiL82pDUeT7nhs8IsuXgfgnsEloKCT4miAV9N9kGtx7/KM9NH/NCGUE7Edt9iGxLXFw==",
+      "version": "7.33.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.1.tgz",
+      "integrity": "sha512-L093k0WAMvr6VhNwReB8VgOq5s2LesZmrpPdKz/kZElQDzqS7G7+DnKoqT+w4JwuiGeAhAvHO0fvy0Eyk4ejDA==",
       "dev": true,
       "dependencies": {
         "array-includes": "^3.1.6",
@@ -5147,9 +5147,9 @@
       }
     },
     "node_modules/eslint-visitor-keys": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
-      "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz",
+      "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -5159,9 +5159,9 @@
       }
     },
     "node_modules/eslint/node_modules/eslint-scope": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
-      "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
       "dev": true,
       "dependencies": {
         "esrecurse": "^4.3.0",
@@ -5187,9 +5187,9 @@
       }
     },
     "node_modules/espree": {
-      "version": "9.6.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz",
-      "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==",
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
       "dev": true,
       "dependencies": {
         "acorn": "^8.9.0",
@@ -9398,9 +9398,9 @@
       }
     },
     "node_modules/prettier": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
-      "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz",
+      "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==",
       "dev": true,
       "bin": {
         "prettier": "bin/prettier.cjs"
@@ -9425,9 +9425,9 @@
       }
     },
     "node_modules/pretty-format": {
-      "version": "29.6.1",
-      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.1.tgz",
-      "integrity": "sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog==",
+      "version": "29.6.2",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz",
+      "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==",
       "dev": true,
       "dependencies": {
         "@jest/schemas": "^29.6.0",
@@ -10330,9 +10330,9 @@
       }
     },
     "node_modules/sass": {
-      "version": "1.64.1",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.1.tgz",
-      "integrity": "sha512-16rRACSOFEE8VN7SCgBu1MpYCyN7urj9At898tyzdXFhC+a+yOX5dXwAR7L8/IdPJ1NB8OYoXmD55DM30B2kEQ==",
+      "version": "1.64.2",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.2.tgz",
+      "integrity": "sha512-TnDlfc+CRnUAgLO9D8cQLFu/GIjJIzJCGkE7o4ekIGQOH7T3GetiRR/PsTWJUHhkzcSPrARkPI+gNWn5alCzDg==",
       "devOptional": true,
       "dependencies": {
         "chokidar": ">=3.0.0 <4.0.0",
@@ -10543,9 +10543,9 @@
       }
     },
     "node_modules/solid-js": {
-      "version": "1.7.8",
-      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.8.tgz",
-      "integrity": "sha512-XHBWk1FvFd0JMKljko7FfhefJMTSgYEuVKcQ2a8hzRXfiuSJAGsrPPafqEo+f6l+e8Oe3cROSpIL6kbzjC1fjQ==",
+      "version": "1.7.9",
+      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.9.tgz",
+      "integrity": "sha512-p1orXnauMQmwYULZtuPAXyKNRGEN2qh60kLX4YURa3jvulxAqjlh2kWEljXCtAVR6UZPC16NXdj9ASHcH383Fg==",
       "dependencies": {
         "csstype": "^3.1.0",
         "seroval": "^0.5.0"
@@ -11551,9 +11551,9 @@
       "dev": true
     },
     "node_modules/tinypool": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.6.0.tgz",
-      "integrity": "sha512-FdswUUo5SxRizcBc6b1GSuLpLjisa8N8qMyYoP3rl+bym+QauhtJP5bvZY1ytt8krKGmMLYIRl36HBZfeAoqhQ==",
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz",
+      "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==",
       "dev": true,
       "engines": {
         "node": ">=14.0.0"
@@ -11795,9 +11795,9 @@
       }
     },
     "node_modules/ufo": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz",
-      "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.2.0.tgz",
+      "integrity": "sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==",
       "dev": true
     },
     "node_modules/unbox-primitive": {
@@ -12486,9 +12486,9 @@
       }
     },
     "node_modules/vite": {
-      "version": "4.4.7",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
-      "integrity": "sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw==",
+      "version": "4.4.8",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.8.tgz",
+      "integrity": "sha512-LONawOUUjxQridNWGQlNizfKH89qPigK36XhMI7COMGztz8KNY0JHim7/xDd71CZwGT4HtSRgI7Hy+RlhG0Gvg==",
       "dependencies": {
         "esbuild": "^0.18.10",
         "postcss": "^8.4.26",
@@ -12540,9 +12540,9 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "0.33.0",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.33.0.tgz",
-      "integrity": "sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==",
+      "version": "0.34.1",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.1.tgz",
+      "integrity": "sha512-odAZAL9xFMuAg8aWd7nSPT+hU8u2r9gU3LRm9QKjxBEF2rRdWpMuqkrkjvyVQEdNFiBctqr2Gg4uJYizm5Le6w==",
       "dev": true,
       "dependencies": {
         "cac": "^6.7.14",
@@ -12667,19 +12667,19 @@
       }
     },
     "node_modules/vitest": {
-      "version": "0.33.0",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.33.0.tgz",
-      "integrity": "sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==",
+      "version": "0.34.1",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.1.tgz",
+      "integrity": "sha512-G1PzuBEq9A75XSU88yO5G4vPT20UovbC/2osB2KEuV/FisSIIsw7m5y2xMdB7RsAGHAfg2lPmp2qKr3KWliVlQ==",
       "dev": true,
       "dependencies": {
         "@types/chai": "^4.3.5",
         "@types/chai-subset": "^1.3.3",
         "@types/node": "*",
-        "@vitest/expect": "0.33.0",
-        "@vitest/runner": "0.33.0",
-        "@vitest/snapshot": "0.33.0",
-        "@vitest/spy": "0.33.0",
-        "@vitest/utils": "0.33.0",
+        "@vitest/expect": "0.34.1",
+        "@vitest/runner": "0.34.1",
+        "@vitest/snapshot": "0.34.1",
+        "@vitest/spy": "0.34.1",
+        "@vitest/utils": "0.34.1",
         "acorn": "^8.9.0",
         "acorn-walk": "^8.2.0",
         "cac": "^6.7.14",
@@ -12692,9 +12692,9 @@
         "std-env": "^3.3.3",
         "strip-literal": "^1.0.1",
         "tinybench": "^2.5.0",
-        "tinypool": "^0.6.0",
+        "tinypool": "^0.7.0",
         "vite": "^3.0.0 || ^4.0.0",
-        "vite-node": "0.33.0",
+        "vite-node": "0.34.1",
         "why-is-node-running": "^2.2.2"
       },
       "bin": {
diff --git a/package.json b/package.json
index c8fba79b..b322a550 100644
--- a/package.json
+++ b/package.json
@@ -26,11 +26,11 @@
   },
   "homepage": "https://web-scrobbler.com/",
   "dependencies": {
-    "@suid/icons-material": "0.6.6",
+    "@suid/icons-material": "0.6.9",
     "@web-scrobbler/metadata-filter": "3.0.1",
     "blueimp-md5": "2.19.0",
     "stylelint": "13.13.1",
-    "vite": "4.4.7",
+    "vite": "4.4.8",
     "webextension-polyfill": "0.10.0"
   },
   "devDependencies": {
@@ -39,17 +39,17 @@
     "@types/fs-extra": "11.0.1",
     "@types/imagemin": "8.0.1",
     "@types/imagemin-jpegtran": "5.0.1",
-    "@types/node": "20.4.4",
-    "@types/react": "18.2.15",
+    "@types/node": "20.4.8",
+    "@types/react": "18.2.18",
     "@types/webextension-polyfill": "0.10.1",
     "@typescript-eslint/eslint-plugin": "5.60.1",
     "archiver": "5.3.1",
     "canvas": "2.11.2",
     "chokidar": "3.5.3",
-    "esbuild": "0.18.15",
-    "eslint": "8.45.0",
+    "esbuild": "0.18.17",
+    "eslint": "8.46.0",
     "eslint-config-web-scrobbler": "2.1.1",
-    "eslint-plugin-react": "7.33.0",
+    "eslint-plugin-react": "7.33.1",
     "eslint-plugin-solid": "0.12.1",
     "eslint-plugin-tsdoc": "0.2.17",
     "fs-extra": "11.1.1",
@@ -57,20 +57,20 @@
     "imagemin": "8.0.1",
     "imagemin-jpegtran": "7.0.0",
     "imagemin-pngquant": "9.0.2",
-    "prettier": "3.0.0",
+    "prettier": "3.0.1",
     "prettier-config-web-scrobbler": "0.1.0",
     "remark": "14.0.3",
     "remark-cli": "11.0.0",
     "remark-preset-lint-recommended": "6.1.3",
-    "sass": "1.64.1",
-    "solid-js": "1.7.8",
+    "sass": "1.64.2",
+    "solid-js": "1.7.9",
     "stylelint-config-web-scrobbler": "0.2.0",
     "tsx": "3.12.7",
     "typescript": "5.1.6",
     "vite-plugin-conditional-compiler": "0.1.1",
     "vite-plugin-solid": "2.7.0",
     "vite-plugin-static-copy": "0.17.0",
-    "vitest": "0.33.0"
+    "vitest": "0.34.1"
   },
   "engines": {
     "node": ">=18.0.0"
diff --git a/scripts/log.ts b/scripts/log.ts
index 465d6014..672086ec 100644
--- a/scripts/log.ts
+++ b/scripts/log.ts
@@ -1,6 +1,6 @@
 type ColorType = 'success' | 'info' | 'error' | 'warning' | keyof typeof COLORS;
 
-export default function colorLog(message: any, type?: ColorType): void {
+export default function colorLog(message: string, type?: ColorType): void {
 	let color: string = type || COLORS.FgBlack;
 
 	switch (type) {
diff --git a/src/connectors/deezer-dom-inject.ts b/src/connectors/deezer-dom-inject.ts
index 734bc197..071d3cbc 100644
--- a/src/connectors/deezer-dom-inject.ts
+++ b/src/connectors/deezer-dom-inject.ts
@@ -1,3 +1,5 @@
+import { State } from '@/core/types';
+
 deezerInitConnector();
 
 function deezerInitConnector() {
@@ -23,7 +25,16 @@ function deezerAddEventListeners() {
 	if (!('Events' in window)) {
 		return;
 	}
-	const ev = window.Events as any;
+	const ev = window.Events as {
+		player: {
+			play: unknown;
+			playing: unknown;
+			paused: unknown;
+			resume: unknown;
+			finish: unknown;
+		};
+		subscribe: (_: unknown, __: unknown) => void;
+	};
 	ev.subscribe(ev.player.play, deezerSendEvent);
 	ev.subscribe(ev.player.playing, deezerSendEvent);
 	ev.subscribe(ev.player.paused, deezerSendEvent);
@@ -40,15 +51,33 @@ function deezerSendEvent() {
 			isPlaying: deezerIsPlaying(),
 			isPodcast: deezerIsPodcast(),
 		},
-		'*'
+		'*',
 	);
 }
 
+interface DeezerMedia {
+	__TYPE__: string;
+	EXTERNAL?: unknown;
+	SNG_ID?: string;
+	SNG_TITLE?: string;
+	VERSION?: string;
+	ART_NAME?: string;
+	ALB_TITLE?: string;
+	ALB_PICTURE: string;
+	SHOW_NAME?: string;
+	EPISODE_TITLE?: string;
+	EPISODE_ID?: string;
+}
+
 function deezerGetCurrentMediaInfo() {
 	if (!('dzPlayer' in window)) {
 		return;
 	}
-	const player = window.dzPlayer as any;
+	const player = window.dzPlayer as {
+		getCurrentSong: () => DeezerMedia;
+		getPosition: () => number | null;
+		getDuration: () => number | null;
+	};
 	const currentMedia = player.getCurrentSong();
 
 	// Radio stations don't provide track info
@@ -60,13 +89,7 @@ function deezerGetCurrentMediaInfo() {
 	const currentTime = player.getPosition();
 	const duration = player.getDuration();
 
-	let trackInfo: {
-		artist: any;
-		track: any;
-		uniqueID: any;
-		duration?: any;
-		currentTime?: any;
-	} | null = null;
+	let trackInfo: State | null = null;
 
 	switch (mediaType) {
 		case 'episode': {
@@ -83,7 +106,7 @@ function deezerGetCurrentMediaInfo() {
 	if (!trackInfo) {
 		Util.debugLog(
 			`Unable to load track info for ${mediaType} media type`,
-			'warn'
+			'warn',
 		);
 		return null;
 	}
@@ -94,7 +117,7 @@ function deezerGetCurrentMediaInfo() {
 	return trackInfo;
 }
 
-function deezerGetTrackInfo(currentMedia: any) {
+function deezerGetTrackInfo(currentMedia: DeezerMedia) {
 	let trackTitle = currentMedia.SNG_TITLE;
 	const trackVersion = currentMedia.VERSION;
 	if (trackVersion) {
@@ -110,7 +133,7 @@ function deezerGetTrackInfo(currentMedia: any) {
 	};
 }
 
-function deezerGetEpisodeInfo(currentMedia: any) {
+function deezerGetEpisodeInfo(currentMedia: DeezerMedia) {
 	return {
 		artist: currentMedia.SHOW_NAME,
 		track: currentMedia.EPISODE_TITLE,
@@ -122,14 +145,16 @@ function deezerIsPlaying() {
 	if (!('dzPlayer' in window)) {
 		return;
 	}
-	return (window.dzPlayer as any).isPlaying();
+	return (window.dzPlayer as { isPlaying: () => boolean }).isPlaying();
 }
 
 function deezerIsPodcast() {
 	if (!('dzPlayer' in window)) {
 		return;
 	}
-	const currentMedia = (window.dzPlayer as any).getCurrentSong();
+	const currentMedia = (
+		window.dzPlayer as { getCurrentSong: () => DeezerMedia }
+	).getCurrentSong();
 	return currentMedia.__TYPE__ === 'episode';
 }
 
diff --git a/src/connectors/deezer.ts b/src/connectors/deezer.ts
index b7d37f5f..0d27d774 100644
--- a/src/connectors/deezer.ts
+++ b/src/connectors/deezer.ts
@@ -1,3 +1,4 @@
+import { State } from '@/core/types';
 export {};
 
 const deezerFilter = MetadataFilter.createRemasteredFilter();
@@ -18,7 +19,7 @@ Connector.onScriptEvent = (event) => {
 	switch (event.data.type) {
 		case 'DEEZER_STATE':
 			({ trackInfo, isPlaying, isPodcast } = event.data as {
-				trackInfo: any;
+				trackInfo: State;
 				isPlaying: boolean;
 				isPodcast: boolean;
 			});
diff --git a/src/connectors/eggs-dom-inject.ts b/src/connectors/eggs-dom-inject.ts
index b98722da..1806eec9 100644
--- a/src/connectors/eggs-dom-inject.ts
+++ b/src/connectors/eggs-dom-inject.ts
@@ -16,10 +16,20 @@ if (eggsIsArtistPage) {
 	observer.observe(document.body, { childList: true });
 } else {
 	if ('player' in window) {
-		(window.player as any).addEventListener(
-			'onStateChange',
-			eggsOnYoutubeSongStateChange
-		);
+		(
+			window.player as {
+				addEventListener: (
+					_: string,
+					__: (ev: {
+						data: number;
+						target: {
+							getCurrentTime: () => number;
+							getDuration: () => number;
+						};
+					}) => void,
+				) => void;
+			}
+		).addEventListener('onStateChange', eggsOnYoutubeSongStateChange);
 	}
 }
 
@@ -35,7 +45,7 @@ function eggsToggleExternalPlayer(mutationList: MutationRecord[]) {
 		// external player has been closed
 		if (
 			(removedList[0] as HTMLElement).classList.contains(
-				'fancybox-overlay'
+				'fancybox-overlay',
 			)
 		) {
 			eggsOnYoutubeClose();
@@ -46,7 +56,7 @@ function eggsToggleExternalPlayer(mutationList: MutationRecord[]) {
 
 function eggsReplaceYoutubeVideo() {
 	const videoFrame = document.querySelector(
-		'.fancybox-inner iframe'
+		'.fancybox-inner iframe',
 	) as HTMLIFrameElement;
 	eggsVideoId = videoFrame.src.split('/').pop()?.split('?')[0];
 
@@ -62,7 +72,7 @@ function eggsReplaceYoutubeVideo() {
 		videoFrame.contentWindow &&
 			videoFrame.contentWindow.postMessage(
 				message,
-				'https://www.youtube.com'
+				'https://www.youtube.com',
 			);
 
 		message = JSON.stringify({
@@ -75,7 +85,7 @@ function eggsReplaceYoutubeVideo() {
 		videoFrame.contentWindow &&
 			videoFrame.contentWindow.postMessage(
 				message,
-				'https://www.youtube.com'
+				'https://www.youtube.com',
 			);
 	});
 }
@@ -95,7 +105,7 @@ window.addEventListener('message', (event) => {
 	}
 });
 
-function eggsOnYoutubeStateChange(data: any) {
+function eggsOnYoutubeStateChange(data: { info: number }) {
 	const currentPlayer = document.querySelector(`a[href*="${eggsVideoId}"]`);
 	const parentElmt =
 		(currentPlayer && currentPlayer.closest('li')) || document;
@@ -112,14 +122,14 @@ function eggsOnYoutubeStateChange(data: any) {
 			},
 			trackInfo: {
 				artist: parentElmt.querySelector(
-					`.artist_name${eggsIsArtistPage ? '' : ' a'}`
+					`.artist_name${eggsIsArtistPage ? '' : ' a'}`,
 				)?.textContent,
 				track: parentElmt.querySelector(
-					`.product_name${eggsIsArtistPage ? ' a' : ' p'}`
+					`.product_name${eggsIsArtistPage ? ' a' : ' p'}`,
 				)?.textContent,
 			},
 		},
-		'*'
+		'*',
 	);
 }
 
@@ -138,18 +148,21 @@ function eggsOnYoutubeClose() {
 			},
 			trackInfo: {
 				artist: parentElmt.querySelector(
-					`.artist_name${eggsIsArtistPage ? '' : ' a'}`
+					`.artist_name${eggsIsArtistPage ? '' : ' a'}`,
 				)?.textContent,
 				track: parentElmt.querySelector(
-					`.product_name${eggsIsArtistPage ? ' a' : ' p'}`
+					`.product_name${eggsIsArtistPage ? ' a' : ' p'}`,
 				)?.textContent,
 			},
 		},
-		'*'
+		'*',
 	);
 }
 
-function eggsOnYoutubeSongStateChange(event: any) {
+function eggsOnYoutubeSongStateChange(event: {
+	data: number;
+	target: { getCurrentTime: () => number; getDuration: () => number };
+}) {
 	const currentPlayer = document.querySelector(`a[href*="${eggsVideoId}"]`);
 	const parentElmt =
 		(currentPlayer && currentPlayer.closest('li')) || document;
@@ -166,18 +179,20 @@ function eggsOnYoutubeSongStateChange(event: any) {
 			},
 			trackInfo: {
 				artist: parentElmt.querySelector(
-					`.artist_name${eggsIsArtistPage ? '' : ' a'}`
+					`.artist_name${eggsIsArtistPage ? '' : ' a'}`,
 				)?.textContent,
 				track: parentElmt.querySelector(
-					`.product_name${eggsIsArtistPage ? ' a' : ' p'}`
+					`.product_name${eggsIsArtistPage ? ' a' : ' p'}`,
 				)?.textContent,
 			},
 		},
-		'*'
+		'*',
 	);
 }
 
-function eggsGetTimestamps(data: any) {
+function eggsGetTimestamps(data: {
+	info?: { currentTime?: number; duration?: number };
+}) {
 	if (data.info) {
 		if (data.info.currentTime) {
 			eggsCurrentTime = data.info.currentTime;
diff --git a/src/connectors/eggs.ts b/src/connectors/eggs.ts
index 9a10ed5d..db8ef103 100644
--- a/src/connectors/eggs.ts
+++ b/src/connectors/eggs.ts
@@ -10,7 +10,7 @@ let isPlaying = false;
 
 Connector.getTimeInfo = () => {
 	const { currentTime, duration } = document.querySelector(
-		'#aPlayer'
+		'#aPlayer',
 	) as HTMLAudioElement;
 	return { currentTime, duration };
 };
@@ -83,7 +83,7 @@ function checkToggleArtist(mutationList: MutationRecord[]) {
 		// external player has been closed
 		if (
 			(removedList[0] as HTMLElement).classList.contains(
-				'fancybox-overlay'
+				'fancybox-overlay',
 			)
 		) {
 			setArtistConnector();
@@ -91,8 +91,10 @@ function checkToggleArtist(mutationList: MutationRecord[]) {
 	}
 }
 
-Connector.onScriptEvent = (event: any) => {
-	({ trackInfo, isPlaying, timeInfo } = event.data);
+Connector.onScriptEvent = (event: MessageEvent<Record<string, unknown>>) => {
+	trackInfo = event.data.trackInfo as object;
+	isPlaying = event.data.isPlaying as boolean;
+	timeInfo = event.data.timeInfo as object;
 
 	if (event.data.playerType === 'youtube') {
 		Connector.onStateChanged();
diff --git a/src/connectors/musickit-dom-inject.ts b/src/connectors/musickit-dom-inject.ts
index e69e73e4..05f7706e 100644
--- a/src/connectors/musickit-dom-inject.ts
+++ b/src/connectors/musickit-dom-inject.ts
@@ -5,19 +5,48 @@
  */
 musicKitAddEventListeners();
 
+interface MusicKitWindow {
+	MusicKit: {
+		Events: {
+			metadataDidChange: unknown;
+			playbackStateDidChange: unknown;
+			nowPlayingItemDidChange: unknown;
+		};
+		getInstance: () => MusicKitInstance;
+	};
+}
+
+interface MusicKitInstance {
+	player?: MusicKitInstance;
+	addEventListener: (_: unknown, __: () => void) => void;
+	nowPlayingItem: {
+		albumName: string;
+		title: string;
+		artistName: string;
+		artworkURL: string;
+		container?: {
+			attributes?: { [key: string]: string };
+		};
+		id: unknown;
+	};
+	currentPlaybackDuration: number;
+	currentPlaybackTime: number;
+	isPlaying: boolean;
+}
+
 async function musicKitAddEventListeners() {
-	const Events = (window as any).MusicKit.Events;
+	const Events = (window as unknown as MusicKitWindow).MusicKit.Events;
 	const instance = await musicKitInstance();
 	instance.addEventListener(Events.metadataDidChange, musicKitSendEvent);
 	instance.addEventListener(Events.playbackStateDidChange, musicKitSendEvent);
 	instance.addEventListener(
 		Events.nowPlayingItemDidChange,
-		musicKitSendEvent
+		musicKitSendEvent,
 	);
 }
 
 async function musicKitInstance() {
-	const api = (window as any).MusicKit;
+	const api = (window as unknown as MusicKitWindow).MusicKit;
 
 	const initInstance = api?.getInstance();
 	// If the instance is already available, return it immediately
@@ -27,7 +56,7 @@ async function musicKitInstance() {
 
 	// Otherwise, poll until it is available
 	let i = 0;
-	return new Promise<any>((resolve, reject) => {
+	return new Promise<MusicKitInstance>((resolve, reject) => {
 		const interval = setInterval(() => {
 			const instance = api?.getInstance();
 			if (instance) {
@@ -52,7 +81,7 @@ async function musicKitSendEvent() {
 			trackInfo: await musicKitGetTrackInfo(),
 			isPlaying: await musicKitIsPlaying(),
 		},
-		'*'
+		'*',
 	);
 }
 
diff --git a/src/connectors/musickit.ts b/src/connectors/musickit.ts
index d8634b02..c7e96261 100644
--- a/src/connectors/musickit.ts
+++ b/src/connectors/musickit.ts
@@ -14,7 +14,7 @@ Connector.getTrackInfo = () => trackInfo;
 Connector.onScriptEvent = (e) => {
 	switch (e.data.type) {
 		case 'MUSICKIT_STATE':
-			trackInfo = e.data.trackInfo as any;
+			trackInfo = e.data.trackInfo as object;
 			isPlaying = e.data.isPlaying as boolean;
 
 			Connector.onStateChanged();
diff --git a/src/connectors/plex.ts b/src/connectors/plex.ts
index 010302b7..b8ad0ac0 100644
--- a/src/connectors/plex.ts
+++ b/src/connectors/plex.ts
@@ -6,7 +6,12 @@ const plexFilter = MetadataFilter.createFilter({
 	album: removeUnknownAlbum,
 });
 
-Connector.playerSelector = '[class^=Player-miniPlayerContainer]';
+Connector.useMediaSessionApi();
+
+Connector.playerSelector = [
+	'[class^=PlayerContainer-container]',
+    '[class^=Player-miniPlayerContainer]',
+];
 
 Connector.trackSelector = `${titleContainer} a[class*=MetadataPosterTitle]`;
 
diff --git a/src/connectors/sber-zvuk-dom-inject.ts b/src/connectors/sber-zvuk-dom-inject.ts
index f9fc3107..212ecf17 100644
--- a/src/connectors/sber-zvuk-dom-inject.ts
+++ b/src/connectors/sber-zvuk-dom-inject.ts
@@ -1,4 +1,19 @@
-function sberGetTrackInfo(track: any, currentTime: number) {
+interface TrackInfo {
+	title: string;
+	credits: string;
+	duration: number;
+	id: string;
+	image: { src: string };
+	release_title: string;
+}
+
+interface QueueEventObject {
+	Queue: QueueEventObject;
+	currentTrack: TrackInfo;
+	Player: { currentTime: number };
+}
+
+function sberGetTrackInfo(track: TrackInfo, currentTime: number) {
 	const { title, credits, duration, id, image, release_title: album } = track;
 	const trackArt = image.src.replace('{size}', '400x400');
 
@@ -13,7 +28,12 @@ function sberGetTrackInfo(track: any, currentTime: number) {
 	};
 }
 
-function sberQueueEvent(event: any) {
+function sberQueueEvent(event: {
+	type: string;
+	name: string;
+	newValue: unknown;
+	object: QueueEventObject;
+}) {
 	let type = null;
 	let queue;
 
@@ -40,7 +60,7 @@ function sberQueueEvent(event: any) {
 
 	const trackInfo = sberGetTrackInfo(
 		queue.currentTrack,
-		queue.Player.currentTime
+		queue.Player.currentTime,
 	);
 
 	window.postMessage(
@@ -49,12 +69,16 @@ function sberQueueEvent(event: any) {
 			type,
 			trackInfo,
 		},
-		'*'
+		'*',
 	);
 }
 
 function sberSetupEventListeners() {
-	(window as any).newPlayer.$mobx.observe(sberQueueEvent);
+	(
+		window as unknown as {
+			newPlayer: { $mobx: { observe: (ev: object) => void } };
+		}
+	).newPlayer.$mobx.observe(sberQueueEvent);
 }
 
 sberSetupEventListeners();
diff --git a/src/connectors/sber-zvuk.ts b/src/connectors/sber-zvuk.ts
index 990d352b..044db261 100644
--- a/src/connectors/sber-zvuk.ts
+++ b/src/connectors/sber-zvuk.ts
@@ -17,7 +17,7 @@ Connector.onScriptEvent = (event) => {
 			break;
 	}
 
-	trackInfo = event.data.trackInfo as any;
+	trackInfo = event.data.trackInfo as object;
 
 	Connector.onStateChanged();
 };
diff --git a/src/connectors/trackerhub.ts b/src/connectors/trackerhub.ts
new file mode 100644
index 00000000..d312091e
--- /dev/null
+++ b/src/connectors/trackerhub.ts
@@ -0,0 +1,4 @@
+export {};
+
+Connector.useMediaSessionApi();
+Connector.playerSelector = '#player';
diff --git a/src/connectors/vk-dom-inject.ts b/src/connectors/vk-dom-inject.ts
index 16b9024e..be93500f 100644
--- a/src/connectors/vk-dom-inject.ts
+++ b/src/connectors/vk-dom-inject.ts
@@ -13,14 +13,24 @@ const VK_INFO_ADDITIONAL = 16;
 
 vkSetupEventListeners();
 
+interface VKWindow {
+	ap: {
+		_currentAudio?: string[];
+		_impl: { _currentAudioEl?: { currentTime: string } };
+		subscribers: {
+			push: (_: object) => void;
+		};
+	};
+}
+
 function vkSendUpdateEvent(type: string) {
-	const audioObject = (window as any).ap._currentAudio;
+	const audioObject = (window as unknown as VKWindow).ap._currentAudio;
 	if (!audioObject) {
 		return;
 	}
 
-	const audioElImpl = (window as any).ap._impl._currentAudioEl || {};
-	const { currentTime } = audioElImpl;
+	const currentTime = (window as unknown as VKWindow).ap._impl._currentAudioEl
+		?.currentTime;
 
 	/*
 	 * VK player sets current time equal to song duration on startup.
@@ -52,13 +62,13 @@ function vkSendUpdateEvent(type: string) {
 				artist: audioObject[VK_INFO_ARTIST],
 			},
 		},
-		'*'
+		'*',
 	);
 }
 
 function vkSetupEventListeners() {
 	for (const e of ['start', 'progress', 'pause', 'stop']) {
-		(window as any).ap.subscribers.push({
+		(window as unknown as VKWindow).ap.subscribers.push({
 			et: e,
 			cb: vkSendUpdateEvent.bind(null, e),
 		});
diff --git a/src/connectors/vk.ts b/src/connectors/vk.ts
index e9975356..3f4e0a4b 100644
--- a/src/connectors/vk.ts
+++ b/src/connectors/vk.ts
@@ -6,8 +6,8 @@ let isPlaying = false;
 const vkFilter = MetadataFilter.createFilter(
 	MetadataFilter.createFilterSetForFields(
 		['artist', 'track', 'album', 'albumArtist'],
-		MetadataFilter.decodeHtmlEntities
-	)
+		MetadataFilter.decodeHtmlEntities,
+	),
 ).extend(MetadataFilter.createRemasteredFilter());
 
 Connector.isPlaying = () => isPlaying;
@@ -25,7 +25,7 @@ Connector.onScriptEvent = (event) => {
 			break;
 	}
 
-	trackInfo = event.data.trackInfo as any;
+	trackInfo = event.data.trackInfo as object;
 
 	Connector.onStateChanged();
 };
diff --git a/src/connectors/yandex-music-dom-inject.ts b/src/connectors/yandex-music-dom-inject.ts
index 06810ffc..b89729b1 100644
--- a/src/connectors/yandex-music-dom-inject.ts
+++ b/src/connectors/yandex-music-dom-inject.ts
@@ -1,4 +1,22 @@
-const YandexAPI = (window as any).externalAPI;
+interface YandexWindow {
+	externalAPI: {
+		on: (_: unknown, __: () => void) => void;
+		EVENT_STATE: unknown;
+		EVENT_TRACK: unknown;
+		isPlaying: () => boolean;
+		getCurrentTrack: () => {
+			title: string;
+			album: { title: string };
+			cover: string;
+			artists: { title: string }[];
+			duration: number;
+			link: string;
+		};
+		getProgress: () => { position: number };
+	};
+}
+
+const YandexAPI = (window as unknown as YandexWindow).externalAPI;
 
 yandexSetupListeners();
 
@@ -15,7 +33,7 @@ function yandexOnEvent() {
 			trackInfo: yandexGetTrackInfo(),
 			isPlaying: YandexAPI.isPlaying(),
 		},
-		'*'
+		'*',
 	);
 }
 
diff --git a/src/connectors/yandex-music.ts b/src/connectors/yandex-music.ts
index d3865739..42e84c7b 100644
--- a/src/connectors/yandex-music.ts
+++ b/src/connectors/yandex-music.ts
@@ -10,7 +10,7 @@ Connector.getTrackInfo = () => trackInfo;
 Connector.onScriptEvent = (e) => {
 	switch (e.data.type) {
 		case 'YANDEX_MUSIC_STATE':
-			trackInfo = e.data.trackInfo as any;
+			trackInfo = e.data.trackInfo as object;
 			isPlaying = e.data.isPlaying as boolean;
 
 			Connector.onStateChanged();
diff --git a/src/connectors/youtube.ts b/src/connectors/youtube.ts
index ac62daf0..6688a736 100644
--- a/src/connectors/youtube.ts
+++ b/src/connectors/youtube.ts
@@ -65,7 +65,14 @@ let getTrackInfoFromYtMusicEnabled = false;
 let currentVideoDescription: string | null = null;
 let artistTrackFromDescription: TrackInfoWithAlbum | null = null;
 
-const getTrackInfoFromYoutubeMusicCache: any = {};
+const getTrackInfoFromYoutubeMusicCache: {
+	[videoId: string]: {
+		done?: boolean;
+		recognisedByYtMusic?: boolean;
+		videoId?: string | null;
+		currentTrackInfo?: { artist?: string; track?: string };
+	};
+} = {};
 
 const trackInfoGetters = [
 	getTrackInfoFromChapters,
@@ -117,7 +124,7 @@ Connector.getTrackInfo = () => {
 
 Connector.getTimeInfo = () => {
 	const videoElement = document.querySelector(
-		videoSelector
+		videoSelector,
 	) as HTMLVideoElement;
 	if (videoElement && !areChaptersAvailable()) {
 		let { currentTime, duration, playbackRate } = videoElement;
@@ -187,7 +194,7 @@ Connector.applyFilter(
 	MetadataFilter.createYouTubeFilter().append({
 		artist: [removeLtrRtlChars, removeNumericPrefix],
 		track: [removeLtrRtlChars, removeNumericPrefix],
-	})
+	}),
 );
 
 function setupEventListener() {
@@ -202,7 +209,7 @@ function areChaptersAvailable() {
 	// SponsorBlock extension hijacks chapter element. Ignore it.
 	if (
 		document.querySelector(
-			'.ytp-chapter-title-content.sponsorBlock-segment-title'
+			'.ytp-chapter-title-content.sponsorBlock-segment-title',
 		)
 	) {
 		return false;
@@ -220,7 +227,7 @@ function getVideoId() {
 	 */
 	const miniPlayerVideoUrl = Util.getAttrFromSelectors(
 		'ytd-miniplayer[active] [selected] a',
-		'href'
+		'href',
 	);
 	if (miniPlayerVideoUrl) {
 		return Util.getYtVideoIdFromUrl(miniPlayerVideoUrl);
@@ -228,7 +235,7 @@ function getVideoId() {
 
 	const videoIDDesktop = Util.getAttrFromSelectors(
 		'ytd-watch-flexy',
-		'video-id'
+		'video-id',
 	);
 	if (videoIDDesktop) {
 		return videoIDDesktop;
@@ -264,7 +271,7 @@ function getVideoCategory() {
 		.catch((err) => {
 			Util.debugLog(
 				`Failed to fetch category for ${videoId}: ${err}`,
-				'warn'
+				'warn',
 			);
 			categoryCache.set(videoId, categoryUnknown);
 		});
@@ -399,7 +406,7 @@ function getTrackInfoFromYoutubeMusic() {
 				done: true,
 				recognisedByYtMusic:
 					videoInfo.videoDetails?.musicVideoType?.startsWith(
-						'MUSIC_VIDEO_'
+						'MUSIC_VIDEO_',
 					) || false,
 			};
 
@@ -421,7 +428,7 @@ function getTrackInfoFromYoutubeMusic() {
 		.catch((err) => {
 			Util.debugLog(
 				`Failed to fetch youtube music data for ${videoId}: ${err}`,
-				'warn'
+				'warn',
 			);
 			getTrackInfoFromYoutubeMusicCache[videoId ?? ''] = {
 				done: true,
@@ -449,7 +456,7 @@ function getTrackInfoFromChapters() {
 
 function getTrackInfoFromTitle() {
 	let { artist, track } = Util.processYtVideoTitle(
-		Util.getTextFromSelectors(videoTitleSelector)
+		Util.getTextFromSelectors(videoTitleSelector),
 	);
 	if (!artist) {
 		artist = Util.getTextFromSelectors(channelNameSelector);
@@ -480,7 +487,7 @@ function removeNumericPrefix(text: string) {
 
 function isVideoStartedPlaying() {
 	const videoElement = document.querySelector(
-		videoSelector
+		videoSelector,
 	) as HTMLVideoElement;
 	return videoElement && videoElement.currentTime > 0;
 }
diff --git a/src/core/connectors.ts b/src/core/connectors.ts
index 3911266c..41f919ae 100644
--- a/src/core/connectors.ts
+++ b/src/core/connectors.ts
@@ -2106,4 +2106,10 @@ export default <ConnectorMeta[]>[
 		js: 'radcap.js',
 		id: 'radcap',
 	},
+	{
+		label: 'TrackerHub',
+		matches: ['*://trackerhub.vercel.app/*'],
+		js: 'trackerhub.js',
+		id: 'trackerhub',
+	},
 ];
diff --git a/src/core/content/util.ts b/src/core/content/util.ts
index 77bd2514..cba5a511 100644
--- a/src/core/content/util.ts
+++ b/src/core/content/util.ts
@@ -101,7 +101,7 @@ export function stringToSeconds(str: string | null | undefined): number {
  */
 export function findSeparator(
 	str: string | null,
-	separators: Separator[] | null = null
+	separators: Separator[] | null = null,
 ): { index: number; length: number } | null {
 	if (str === null || str.length === 0) {
 		return null;
@@ -145,7 +145,7 @@ export function joinArtists(artists: Node[]): string | null {
 export function splitArtistTrack(
 	str: string | null | undefined,
 	separators: Separator[] | null = null,
-	swap = false
+	swap = false,
 ): ArtistTrackInfo {
 	if (!str) {
 		return { artist: null, track: null };
@@ -181,7 +181,7 @@ export function removeRecordSide(str: string | null): string | null {
 export function splitArtistAlbum(
 	str: string | null | undefined,
 	separators: Separator[] | null = null,
-	swap = false
+	swap = false,
 ): { artist: string | null; album: string | null } {
 	const [artist, album] = splitString(str, separators, swap);
 	return { artist, album };
@@ -197,7 +197,7 @@ export function splitArtistAlbum(
 export function splitTimeInfo(
 	str: string | null,
 	separator: Separator = '/',
-	swap = false
+	swap = false,
 ): TimeInfo {
 	if (str === null) {
 		return {
@@ -206,7 +206,7 @@ export function splitTimeInfo(
 		};
 	}
 	const [currentTime, duration] = splitString(str, [separator], swap).map(
-		(e) => stringToSeconds(e)
+		(e) => stringToSeconds(e),
 	);
 
 	return { currentTime, duration };
@@ -222,7 +222,7 @@ export function splitTimeInfo(
 export function splitString(
 	str: string | null | undefined,
 	separators: Separator[] | null,
-	swap = false
+	swap = false,
 ): [string | null, string | null] {
 	let first = null;
 	let second = null;
@@ -265,7 +265,7 @@ export function escapeBadTimeValues(time: unknown): number | null {
  * @returns Track art URL
  */
 export function extractUrlFromCssProperty(
-	cssProperty: string | null | undefined
+	cssProperty: string | null | undefined,
 ): string | null {
 	if (!cssProperty) {
 		return null;
@@ -313,7 +313,7 @@ export function throttle<T>(func: () => T, wait: number): () => T | undefined {
  * @returns True if object is empty; false otherwise
  */
 export function isArtistTrackEmpty(
-	artistTrack: ArtistTrackInfo | null
+	artistTrack: ArtistTrackInfo | null,
 ): boolean {
 	return !(artistTrack && artistTrack.artist && artistTrack.track);
 }
@@ -329,7 +329,7 @@ export function isArtistTrackEmpty(
 export function fillEmptyFields(
 	target: State,
 	source: State | null | undefined,
-	fields: (keyof State)[] | undefined
+	fields: (keyof State)[] | undefined,
 ): State {
 	if (!source || !Array.isArray(fields)) {
 		return target;
@@ -352,7 +352,7 @@ export function fillEmptyFields(
  * @returns Track info
  */
 export function getMediaSessionInfo(
-	mediaSession: MediaSession
+	mediaSession: MediaSession,
 ): BaseState | null {
 	if (!(mediaSession && mediaSession.metadata)) {
 		return null;
@@ -383,7 +383,7 @@ export function getMediaSessionInfo(
 /* istanbul ignore next */
 export function getTextFromSelectors(
 	selectors: string | string[] | null,
-	defaultValue: string | null = null
+	defaultValue: string | null = null,
 ): string | null {
 	if (selectors === null) {
 		return defaultValue;
@@ -419,7 +419,7 @@ export function getTextFromSelectors(
 export function getAttrFromSelectors(
 	selectors: string | string[] | null,
 	attr: string,
-	defaultValue: string | null = null
+	defaultValue: string | null = null,
 ): string | null {
 	if (selectors === null) {
 		return defaultValue;
@@ -452,7 +452,7 @@ export function getAttrFromSelectors(
 export function bindListeners(
 	selectors: string | string[],
 	events: string | string[],
-	fn: (e: Event) => void
+	fn: (e: Event) => void,
 ) {
 	const elements = queryElements(selectors);
 	if (!elements) {
@@ -478,7 +478,7 @@ export function bindListeners(
  */
 /* istanbul ignore next */
 export function getSecondsFromSelectors(
-	selectors: string | string[] | null
+	selectors: string | string[] | null,
 ): number | undefined {
 	return stringToSeconds(getTextFromSelectors(selectors) ?? '');
 }
@@ -490,7 +490,7 @@ export function getSecondsFromSelectors(
  */
 /* istanbul ignore next */
 export function extractImageUrlFromSelectors(
-	selectors: string | string[] | null
+	selectors: string | string[] | null,
 ): string | null {
 	if (selectors === null) {
 		return null;
@@ -524,7 +524,7 @@ export function extractImageUrlFromSelectors(
  */
 export function getCSSPropertyFromSelectors(
 	selectors: string | string[],
-	property: string
+	property: string,
 ) {
 	const elements = queryElements(selectors);
 	if (!elements) {
@@ -557,7 +557,7 @@ export function getCSSProperty(element: Element, property: string) {
 /* istanbul ignore next */
 export function hasElementClass(
 	selectors: string | string[] | null,
-	cls: string
+	cls: string,
 ): boolean {
 	if (selectors === null) {
 		return false;
@@ -601,7 +601,7 @@ function visibleFilter(elements: NodeListOf<HTMLElement>) {
  */
 /* istanbul ignore next */
 export function isElementVisible(
-	selectors: string | string[] | null | undefined
+	selectors: string | string[] | null | undefined,
 ): boolean {
 	if (!selectors) {
 		return false;
@@ -617,7 +617,7 @@ export function isElementVisible(
  * @returns Value, or null if not found
  */
 export function getValueFromSelectors(
-	selectors: string | string[]
+	selectors: string | string[],
 ): string | null {
 	const element = queryElements(selectors);
 	if (!element || !('value' in element)) {
@@ -635,7 +635,7 @@ export function getValueFromSelectors(
  */
 export function getDataFromSelectors(
 	selectors: string | string[],
-	name: string
+	name: string,
 ): string | null {
 	return getAttrFromSelectors(selectors, `data-${name}`);
 }
@@ -649,7 +649,7 @@ export function getDataFromSelectors(
  */
 /* istanbul ignore next */
 export function queryElements(
-	selectors: string | string[] | null | undefined
+	selectors: string | string[] | null | undefined,
 ): NodeListOf<HTMLElement> | null {
 	if (!selectors) {
 		return null;
@@ -665,7 +665,7 @@ export function queryElements(
 
 	for (const selector of selectors) {
 		const elements = document.querySelectorAll(
-			selector
+			selector,
 		) as NodeListOf<HTMLElement>;
 		if (elements.length > 0) {
 			return elements;
@@ -684,7 +684,7 @@ export function queryElements(
 /* istanbul ignore next */
 export async function getOption(
 	connector: string,
-	key: string
+	key: string,
 ): Promise<boolean> {
 	const awaitedStorage = await BrowserStorage;
 	const data = await awaitedStorage
@@ -747,7 +747,7 @@ export function injectScriptIntoDocument(scriptUrl: string): void {
  * @param logType - Log type
  */
 /* istanbul ignore next */
-export function debugLog(text: any, logType: DebugLogType = 'log'): void {
+export function debugLog(text: unknown, logType: DebugLogType = 'log'): void {
 	const logFunc = console[logType];
 
 	if (typeof logFunc !== 'function') {
@@ -797,7 +797,7 @@ export const ytTitleRegExps = [
  * @returns Object containing artist and track fields
  */
 export function processYtVideoTitle(
-	videoTitle: string | null | undefined
+	videoTitle: string | null | undefined,
 ): ArtistTrackInfo {
 	let artist = null;
 	let track = null;
@@ -861,12 +861,12 @@ export function processYtVideoTitle(
 export function isYtVideoDescriptionValid(desc: string | null): desc is string {
 	return Boolean(
 		desc &&
-			(desc.startsWith(ytDescFirstLine) || desc.endsWith(ytDescLastLine))
+			(desc.startsWith(ytDescFirstLine) || desc.endsWith(ytDescLastLine)),
 	);
 }
 
 export function parseYtVideoDescription(
-	desc: string | null
+	desc: string | null,
 ): TrackInfoWithAlbum | null {
 	if (!isYtVideoDescriptionValid(desc)) {
 		return null;
@@ -899,8 +899,8 @@ export function parseYtVideoDescription(
 	} else {
 		[track, artist, ...featArtists] = trackInfo;
 
-		const areFeatArtistPresent = featArtists.some((artist) =>
-			track?.includes(artist)
+		const areFeatArtistPresent = featArtists.some(
+			(artist) => track?.includes(artist),
 		);
 		if (!areFeatArtistPresent) {
 			const featArtistsStr = featArtists.join(ARTIST_SEPARATOR);
@@ -942,7 +942,7 @@ export const scArtistTrackRe = /(.+)\s[:\u2013-\u2015-]\s(.+)/;
  * @returns Object contains artist and track fields
  */
 export function processSoundCloudTrack(
-	track: string | null | undefined
+	track: string | null | undefined,
 ): ArtistTrackInfo {
 	if (!track) {
 		return { artist: null, track: null };
diff --git a/src/types/declaration.d.ts b/src/types/declaration.d.ts
index d834a6c6..62207cb0 100644
--- a/src/types/declaration.d.ts
+++ b/src/types/declaration.d.ts
@@ -4,6 +4,7 @@ declare module '*.scss' {
 }
 
 declare module '*.svg' {
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	const content: any;
 	export default content;
 }
diff --git a/src/util/communication.ts b/src/util/communication.ts
index a424059c..1cf66619 100644
--- a/src/util/communication.ts
+++ b/src/util/communication.ts
@@ -140,23 +140,23 @@ interface SpecificContentListener<K extends keyof BackgroundCommunications> {
 	type: K;
 	fn: (
 		payload: BackgroundCommunications[K]['payload'],
-		sender: browser.Runtime.MessageSender
+		sender: browser.Runtime.MessageSender,
 	) => BackgroundCommunications[K]['response'];
 }
 
 type ContentListener = <R>(
 	cont: <T extends keyof BackgroundCommunications>(
-		prop: SpecificContentListener<T>
-	) => R
+		prop: SpecificContentListener<T>,
+	) => R,
 ) => R;
 
 export function contentListener<T extends keyof BackgroundCommunications>(
-	property: SpecificContentListener<T>
+	property: SpecificContentListener<T>,
 ): ContentListener {
 	return <R>(
 		cont: <T extends keyof BackgroundCommunications>(
-			prop: SpecificContentListener<T>
-		) => R
+			prop: SpecificContentListener<T>,
+		) => R,
 	) => cont(property);
 }
 
@@ -167,6 +167,7 @@ interface BackgroundMessage<K extends keyof BackgroundCommunications> {
 
 export function setupContentListeners(...listeners: ContentListener[]) {
 	browser.runtime.onMessage.addListener(
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 		(message: BackgroundMessage<any>, sender) => {
 			let done = false;
 			for (const l of listeners) {
@@ -181,15 +182,15 @@ export function setupContentListeners(...listeners: ContentListener[]) {
 					return Promise.resolve(response);
 				}
 			}
-		}
+		},
 	);
 }
 
 export async function sendBackgroundMessage<
-	K extends keyof BackgroundCommunications
+	K extends keyof BackgroundCommunications,
 >(
 	tabId: number,
-	message: BackgroundMessage<K>
+	message: BackgroundMessage<K>,
 ): Promise<BackgroundCommunications[K]['response']> {
 	return browser.tabs.sendMessage(tabId, message);
 }
@@ -202,23 +203,23 @@ interface SpecificBackgroundListener<K extends keyof ContentCommunications> {
 	type: K;
 	fn: (
 		payload: ContentCommunications[K]['payload'],
-		sender: browser.Runtime.MessageSender
+		sender: browser.Runtime.MessageSender,
 	) => ContentCommunications[K]['response'];
 }
 
 type BackgroundListener = <R>(
 	cont: <T extends keyof ContentCommunications>(
-		prop: SpecificBackgroundListener<T>
-	) => R
+		prop: SpecificBackgroundListener<T>,
+	) => R,
 ) => R;
 
 export function backgroundListener<T extends keyof ContentCommunications>(
-	property: SpecificBackgroundListener<T>
+	property: SpecificBackgroundListener<T>,
 ): BackgroundListener {
 	return <R>(
 		cont: <T extends keyof ContentCommunications>(
-			prop: SpecificBackgroundListener<T>
-		) => R
+			prop: SpecificBackgroundListener<T>,
+		) => R,
 	) => cont(property);
 }
 
@@ -229,6 +230,7 @@ interface ContentMessage<K extends keyof ContentCommunications> {
 
 export function setupBackgroundListeners(...listeners: BackgroundListener[]) {
 	browser.runtime.onMessage.addListener(
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 		(message: ContentMessage<any>, sender) => {
 			let done = false;
 			for (const l of listeners) {
@@ -243,12 +245,12 @@ export function setupBackgroundListeners(...listeners: BackgroundListener[]) {
 					return Promise.resolve(response);
 				}
 			}
-		}
+		},
 	);
 }
 
 export async function sendContentMessage<K extends keyof ContentCommunications>(
-	message: ContentMessage<K>
+	message: ContentMessage<K>,
 ): Promise<ContentCommunications[K]['response']> {
 	return browser.runtime.sendMessage(message);
 }
@@ -261,23 +263,23 @@ interface SpecificPopupListener<K extends keyof PopupCommunications> {
 	type: K;
 	fn: (
 		payload: PopupCommunications[K]['payload'],
-		sender: browser.Runtime.MessageSender
+		sender: browser.Runtime.MessageSender,
 	) => PopupCommunications[K]['response'];
 }
 
 type PopupListener = <R>(
 	cont: <T extends keyof PopupCommunications>(
-		prop: SpecificPopupListener<T>
-	) => R
+		prop: SpecificPopupListener<T>,
+	) => R,
 ) => R;
 
 export function popupListener<T extends keyof PopupCommunications>(
-	property: SpecificPopupListener<T>
+	property: SpecificPopupListener<T>,
 ): PopupListener {
 	return <R>(
 		cont: <T extends keyof PopupCommunications>(
-			prop: SpecificPopupListener<T>
-		) => R
+			prop: SpecificPopupListener<T>,
+		) => R,
 	) => cont(property);
 }
 
@@ -288,6 +290,7 @@ interface PopupMessage<K extends keyof PopupCommunications> {
 
 export function setupPopupListeners(...listeners: PopupListener[]) {
 	browser.runtime.onMessage.addListener(
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Set regardless of previous state
 		(message: PopupMessage<any>, sender) => {
 			let done = false;
 			for (const l of listeners) {
@@ -302,12 +305,12 @@ export function setupPopupListeners(...listeners: PopupListener[]) {
 					return Promise.resolve(response);
 				}
 			}
-		}
+		},
 	);
 }
 
 export async function sendPopupMessage<K extends keyof PopupCommunications>(
-	message: PopupMessage<K>
+	message: PopupMessage<K>,
 ): Promise<PopupCommunications[K]['response']> {
 	return browser.runtime.sendMessage(message);
 }
diff --git a/src/util/emitter.ts b/src/util/emitter.ts
index 008de0e0..980a734a 100644
--- a/src/util/emitter.ts
+++ b/src/util/emitter.ts
@@ -2,7 +2,8 @@
  * Class that emulates event emitter behavior while having strict typing
  */
 export default class EventEmitter<
-	T extends Record<string, (...args: any[]) => void>
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	T extends Record<string, (...args: any[]) => void>,
 > {
 	private events: Map<keyof T, T[keyof T][]> = new Map();
 	/**
@@ -26,7 +27,7 @@ export default class EventEmitter<
 		const curEvents = this.events.get(event) || [];
 		this.events.set(
 			event,
-			curEvents.filter((e) => e !== fn)
+			curEvents.filter((e) => e !== fn),
 		);
 	}
 
diff --git a/src/util/regex.ts b/src/util/regex.ts
index c38cb887..36edb784 100644
--- a/src/util/regex.ts
+++ b/src/util/regex.ts
@@ -1,4 +1,3 @@
-import ClonedSong from '@/core/object/cloned-song';
 import Song, { BaseSong } from '@/core/object/song';
 import * as BrowserStorage from '@/core/storage/browser-storage';
 
@@ -107,7 +106,7 @@ export function searchMatches(search: string | null, text: string): boolean {
 function replaceField(
 	search: string | null,
 	replace: string | null,
-	text: string
+	text: string,
 ): string {
 	if (search === null || replace === null) {
 		return text;
@@ -131,7 +130,7 @@ function replaceField(
 export function replaceFields(
 	search: RegexFields,
 	replace: RegexFields,
-	fields: EditedFields
+	fields: EditedFields,
 ): EditedFields {
 	for (const _field of Object.keys(fields)) {
 		const field = _field as FieldType;
@@ -144,7 +143,7 @@ export function replaceFields(
 		fields[field] = replaceField(
 			search[field],
 			replace[field],
-			fields[field]
+			fields[field],
 		);
 	}
 	return fields;
@@ -191,7 +190,7 @@ export function editSong(edit: RegexEdit, song: Song) {
 	const fields = replaceFields(
 		edit.search,
 		edit.replace,
-		getProcessedFields(song)
+		getProcessedFields(song),
 	);
 
 	for (const [_key, field] of Object.entries(fields)) {
@@ -234,7 +233,7 @@ export function getSongField(clonedSong: BaseSong, type: FieldType): string {
  */
 export function getSongFieldNoRegex(
 	clonedSong: BaseSong,
-	type: FieldType
+	type: FieldType,
 ): string {
 	return (clonedSong.noRegex[type] || clonedSong.parsed[type]) ?? '';
 }


Test patch:
diff --git a/tests/background/song.test.ts b/tests/background/song.test.ts
index 0ebcc36e..9808c3a2 100644
--- a/tests/background/song.test.ts
+++ b/tests/background/song.test.ts
@@ -61,7 +61,7 @@ const DUMMY_CONNECTOR = {
  */
 function createSong(
 	parsedData: ParsedSongData,
-	processedData?: ProcessedSongData
+	processedData?: ProcessedSongData,
 ) {
 	const parsedDataCopy: ParsedSongData = {};
 	for (const prop in defaultParsedData) {
@@ -143,7 +143,7 @@ function testGetDuration() {
 	it('should return parsed duration if no processed duration', () => {
 		const song = createSong(
 			{ duration: parsedDuration },
-			{ duration: processedDuration }
+			{ duration: processedDuration },
 		);
 		expect(song.getDuration()).equals(parsedDuration);
 	});
@@ -378,11 +378,11 @@ function testEquals() {
 	});
 
 	it('should not equal null value', () => {
-		expect(songWithUniqueId.equals(null as any)).to.be.false;
+		expect(songWithUniqueId.equals(null)).to.be.false;
 	});
 
 	it('should not equal non-song object', () => {
-		expect(songWithUniqueId.equals(23 as any)).to.be.false;
+		expect(songWithUniqueId.equals(23)).to.be.false;
 	});
 }
 
diff --git a/tests/background/util.test.ts b/tests/background/util.test.ts
index b42d1523..059830d9 100644
--- a/tests/background/util.test.ts
+++ b/tests/background/util.test.ts
@@ -226,7 +226,7 @@ function runTests() {
 function testDebugLog() {
 	it('should throw an error if type is invalid', () => {
 		function callInvalidDebugLog() {
-			Util.debugLog('Test', 'invalid_type123' as any);
+			Util.debugLog('Test', 'invalid_type123');
 		}
 
 		expect(callInvalidDebugLog).to.throw();
@@ -271,7 +271,10 @@ function testTimeoutPromise() {
  * @param func - Function to be tested
  * @param testData - Array of test data
  */
-function testFunction(func: (...args: any[]) => unknown, testData: TestData[]) {
+function testFunction(
+	func: (...args: unknown[]) => unknown,
+	testData: TestData[],
+) {
 	const boundFunc = func.bind(Util);
 
 	for (const data of testData) {
diff --git a/tests/content/util.test.ts b/tests/content/util.test.ts
index a6404a51..3ce7f645 100644
--- a/tests/content/util.test.ts
+++ b/tests/content/util.test.ts
@@ -2,7 +2,7 @@ import { TestData } from '#/types/types';
 import { expect, it, describe } from 'vitest';
 
 // trick webextension polyfill into thinking this is running in the browser
-(globalThis as any).chrome = {
+(globalThis as { chrome: unknown }).chrome = {
 	runtime: {
 		id: 'mock',
 	},
@@ -1098,7 +1098,10 @@ const testData = [
  * @param func - Function to be tested
  * @param testData - Array of test data
  */
-function testFunction(func: (...args: any[]) => unknown, testData: TestData[]) {
+function testFunction(
+	func: (...args: unknown[]) => unknown,
+	testData: TestData[],
+) {
 	const boundFunc = func.bind(Util);
 
 	for (const data of testData) {
diff --git a/tests/mocks/webextension-polyfill.ts b/tests/mocks/webextension-polyfill.ts
index d2431881..76d09c68 100644
--- a/tests/mocks/webextension-polyfill.ts
+++ b/tests/mocks/webextension-polyfill.ts
@@ -27,7 +27,7 @@ class StorageAreaStub {
 }
 
 const messages = {
-	getSongInfo: (payload: any) => {
+	getSongInfo: (payload: { song: CloneableSong }) => {
 		if (
 			payload.song.processed.artist === 'フミンニッキ' &&
 			payload.song.processed.track === 'Re:start'