Repository: web-scrobbler/web-scrobbler
Pull request #4057 (2023-08-26 16:54:31)

Bump @types/react from 18.2.20 to 18.2.21
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.20 to 18.2.21.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/react&package-manager=npm_and_yarn&previous-version=18.2.20&new-version=18.2.21)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/package-lock.json b/package-lock.json
index 9b6afaa5..69586fbf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "web-scrobbler",
-  "version": "3.0.15",
+  "version": "3.1.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "web-scrobbler",
-      "version": "3.0.15",
+      "version": "3.1.0",
       "license": "MIT",
       "dependencies": {
         "@suid/icons-material": "0.6.9",
@@ -23,7 +23,7 @@
         "@types/imagemin": "8.0.1",
         "@types/imagemin-jpegtran": "5.0.1",
         "@types/node": "20.5.6",
-        "@types/react": "18.2.20",
+        "@types/react": "18.2.21",
         "@types/webextension-polyfill": "0.10.1",
         "@typescript-eslint/eslint-plugin": "6.4.1",
         "archiver": "6.0.0",
@@ -36,11 +36,11 @@
         "eslint-plugin-solid": "0.12.1",
         "eslint-plugin-tsdoc": "0.2.17",
         "fs-extra": "11.1.1",
-        "glob": "10.3.3",
+        "glob": "10.3.4",
         "imagemin": "8.0.1",
         "imagemin-jpegtran": "7.0.0",
         "imagemin-pngquant": "9.0.2",
-        "prettier": "3.0.1",
+        "prettier": "3.0.2",
         "prettier-config-web-scrobbler": "0.1.0",
         "remark": "14.0.3",
         "remark-cli": "11.0.0",
@@ -49,11 +49,11 @@
         "solid-js": "1.7.11",
         "stylelint-config-web-scrobbler": "0.2.0",
         "tsx": "3.12.7",
-        "typescript": "5.1.6",
+        "typescript": "5.2.2",
         "vite-plugin-conditional-compiler": "0.1.1",
         "vite-plugin-solid": "2.7.0",
         "vite-plugin-static-copy": "0.17.0",
-        "vitest": "0.34.2"
+        "vitest": "0.34.3"
       },
       "engines": {
         "node": ">=18.0.0"
@@ -1514,9 +1514,9 @@
       "dev": true
     },
     "node_modules/@jest/schemas": {
-      "version": "29.6.0",
-      "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz",
-      "integrity": "sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==",
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+      "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
       "dev": true,
       "dependencies": {
         "@sinclair/typebox": "^0.27.8"
@@ -2369,9 +2369,9 @@
       "dev": true
     },
     "node_modules/@types/react": {
-      "version": "18.2.20",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz",
-      "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==",
+      "version": "18.2.21",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz",
+      "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==",
       "dev": true,
       "dependencies": {
         "@types/prop-types": "*",
@@ -2935,13 +2935,13 @@
       }
     },
     "node_modules/@vitest/expect": {
-      "version": "0.34.2",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.2.tgz",
-      "integrity": "sha512-EZm2dMNlLyIfDMha17QHSQcg2KjeAZaXd65fpPzXY5bvnfx10Lcaz3N55uEe8PhF+w4pw+hmrlHLLlRn9vkBJg==",
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.3.tgz",
+      "integrity": "sha512-F8MTXZUYRBVsYL1uoIft1HHWhwDbSzwAU9Zgh8S6WFC3YgVb4AnFV2GXO3P5Em8FjEYaZtTnQYoNwwBrlOMXgg==",
       "dev": true,
       "dependencies": {
-        "@vitest/spy": "0.34.2",
-        "@vitest/utils": "0.34.2",
+        "@vitest/spy": "0.34.3",
+        "@vitest/utils": "0.34.3",
         "chai": "^4.3.7"
       },
       "funding": {
@@ -2949,12 +2949,12 @@
       }
     },
     "node_modules/@vitest/runner": {
-      "version": "0.34.2",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.2.tgz",
-      "integrity": "sha512-8ydGPACVX5tK3Dl0SUwxfdg02h+togDNeQX3iXVFYgzF5odxvaou7HnquALFZkyVuYskoaHUOqOyOLpOEj5XTA==",
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.3.tgz",
+      "integrity": "sha512-lYNq7N3vR57VMKMPLVvmJoiN4bqwzZ1euTW+XXYH5kzr3W/+xQG3b41xJn9ChJ3AhYOSoweu974S1V3qDcFESA==",
       "dev": true,
       "dependencies": {
-        "@vitest/utils": "0.34.2",
+        "@vitest/utils": "0.34.3",
         "p-limit": "^4.0.0",
         "pathe": "^1.1.1"
       },
@@ -2990,9 +2990,9 @@
       }
     },
     "node_modules/@vitest/snapshot": {
-      "version": "0.34.2",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.2.tgz",
-      "integrity": "sha512-qhQ+xy3u4mwwLxltS4Pd4SR+XHv4EajiTPNY3jkIBLUApE6/ce72neJPSUQZ7bL3EBuKI+NhvzhGj3n5baRQUQ==",
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.3.tgz",
+      "integrity": "sha512-QyPaE15DQwbnIBp/yNJ8lbvXTZxS00kRly0kfFgAD5EYmCbYcA+1EEyRalc93M0gosL/xHeg3lKAClIXYpmUiQ==",
       "dev": true,
       "dependencies": {
         "magic-string": "^0.30.1",
@@ -3004,9 +3004,9 @@
       }
     },
     "node_modules/@vitest/spy": {
-      "version": "0.34.2",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.2.tgz",
-      "integrity": "sha512-yd4L9OhfH6l0Av7iK3sPb3MykhtcRN5c5K5vm1nTbuN7gYn+yvUVVsyvzpHrjqS7EWqn9WsPJb7+0c3iuY60tA==",
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.3.tgz",
+      "integrity": "sha512-N1V0RFQ6AI7CPgzBq9kzjRdPIgThC340DGjdKdPSE8r86aUSmeliTUgkTqLSgtEwWWsGfBQ+UetZWhK0BgJmkQ==",
       "dev": true,
       "dependencies": {
         "tinyspy": "^2.1.1"
@@ -3016,9 +3016,9 @@
       }
     },
     "node_modules/@vitest/utils": {
-      "version": "0.34.2",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.2.tgz",
-      "integrity": "sha512-Lzw+kAsTPubhoQDp1uVAOP6DhNia1GMDsI9jgB0yMn+/nDaPieYQ88lKqz/gGjSHL4zwOItvpehec9OY+rS73w==",
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.3.tgz",
+      "integrity": "sha512-kiSnzLG6m/tiT0XEl4U2H8JDBjFtwVlaE8I3QfGiMFR0QvnRDfYfdP3YvTBWM/6iJDAyaPY6yVQiCTUc7ZzTHA==",
       "dev": true,
       "dependencies": {
         "diff-sequences": "^29.4.3",
@@ -3047,9 +3047,9 @@
       }
     },
     "node_modules/acorn": {
-      "version": "8.9.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
-      "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
+      "version": "8.10.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+      "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
       "dev": true,
       "bin": {
         "acorn": "bin/acorn"
@@ -4169,9 +4169,9 @@
       }
     },
     "node_modules/chai": {
-      "version": "4.3.7",
-      "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz",
-      "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==",
+      "version": "4.3.8",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz",
+      "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==",
       "dev": true,
       "dependencies": {
         "assertion-error": "^1.1.0",
@@ -4881,9 +4881,9 @@
       }
     },
     "node_modules/diff-sequences": {
-      "version": "29.4.3",
-      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz",
-      "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==",
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
+      "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
       "dev": true,
       "engines": {
         "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@@ -6201,9 +6201,9 @@
       }
     },
     "node_modules/glob": {
-      "version": "10.3.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz",
-      "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==",
+      "version": "10.3.4",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz",
+      "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==",
       "dev": true,
       "dependencies": {
         "foreground-child": "^3.1.0",
@@ -8750,15 +8750,15 @@
       }
     },
     "node_modules/mlly": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz",
-      "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==",
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.1.tgz",
+      "integrity": "sha512-SCDs78Q2o09jiZiE2WziwVBEqXQ02XkGdUy45cbJf+BpYRIjArXRJ1Wbowxkb+NaM9DWvS3UC9GiO/6eqvQ/pg==",
       "dev": true,
       "dependencies": {
-        "acorn": "^8.9.0",
+        "acorn": "^8.10.0",
         "pathe": "^1.1.1",
         "pkg-types": "^1.0.3",
-        "ufo": "^1.1.2"
+        "ufo": "^1.3.0"
       }
     },
     "node_modules/mri": {
@@ -9763,9 +9763,9 @@
       }
     },
     "node_modules/prettier": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz",
-      "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz",
+      "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==",
       "dev": true,
       "bin": {
         "prettier": "bin/prettier.cjs"
@@ -9790,12 +9790,12 @@
       }
     },
     "node_modules/pretty-format": {
-      "version": "29.6.2",
-      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz",
-      "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==",
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz",
+      "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==",
       "dev": true,
       "dependencies": {
-        "@jest/schemas": "^29.6.0",
+        "@jest/schemas": "^29.6.3",
         "ansi-styles": "^5.0.0",
         "react-is": "^18.0.0"
       },
@@ -12254,9 +12254,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.1.6",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
-      "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+      "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
       "dev": true,
       "bin": {
         "tsc": "bin/tsc",
@@ -12267,9 +12267,9 @@
       }
     },
     "node_modules/ufo": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.2.0.tgz",
-      "integrity": "sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.0.tgz",
+      "integrity": "sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==",
       "dev": true
     },
     "node_modules/unbox-primitive": {
@@ -13012,9 +13012,9 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "0.34.2",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.2.tgz",
-      "integrity": "sha512-JtW249Zm3FB+F7pQfH56uWSdlltCo1IOkZW5oHBzeQo0iX4jtC7o1t9aILMGd9kVekXBP2lfJBEQt9rBh07ebA==",
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.3.tgz",
+      "integrity": "sha512-+0TzJf1g0tYXj6tR2vEyiA42OPq68QkRZCu/ERSo2PtsDJfBpDyEfuKbRvLmZqi/CgC7SCBtyC+WjTGNMRIaig==",
       "dev": true,
       "dependencies": {
         "cac": "^6.7.14",
@@ -13505,19 +13505,19 @@
       }
     },
     "node_modules/vitest": {
-      "version": "0.34.2",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.2.tgz",
-      "integrity": "sha512-WgaIvBbjsSYMq/oiMlXUI7KflELmzM43BEvkdC/8b5CAod4ryAiY2z8uR6Crbi5Pjnu5oOmhKa9sy7uk6paBxQ==",
+      "version": "0.34.3",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.3.tgz",
+      "integrity": "sha512-7+VA5Iw4S3USYk+qwPxHl8plCMhA5rtfwMjgoQXMT7rO5ldWcdsdo3U1QD289JgglGK4WeOzgoLTsGFu6VISyQ==",
       "dev": true,
       "dependencies": {
         "@types/chai": "^4.3.5",
         "@types/chai-subset": "^1.3.3",
         "@types/node": "*",
-        "@vitest/expect": "0.34.2",
-        "@vitest/runner": "0.34.2",
-        "@vitest/snapshot": "0.34.2",
-        "@vitest/spy": "0.34.2",
-        "@vitest/utils": "0.34.2",
+        "@vitest/expect": "0.34.3",
+        "@vitest/runner": "0.34.3",
+        "@vitest/snapshot": "0.34.3",
+        "@vitest/spy": "0.34.3",
+        "@vitest/utils": "0.34.3",
         "acorn": "^8.9.0",
         "acorn-walk": "^8.2.0",
         "cac": "^6.7.14",
@@ -13532,7 +13532,7 @@
         "tinybench": "^2.5.0",
         "tinypool": "^0.7.0",
         "vite": "^3.0.0 || ^4.0.0",
-        "vite-node": "0.34.2",
+        "vite-node": "0.34.3",
         "why-is-node-running": "^2.2.2"
       },
       "bin": {
diff --git a/package.json b/package.json
index 54dae77e..908042ac 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "web-scrobbler",
-  "version": "3.0.15",
+  "version": "3.1.0",
   "author": "David Šabata <posta@davidsabata.cz>",
   "contributors": [
     "@mountainash (https://mountainash.id.au/)"
@@ -40,7 +40,7 @@
     "@types/imagemin": "8.0.1",
     "@types/imagemin-jpegtran": "5.0.1",
     "@types/node": "20.5.6",
-    "@types/react": "18.2.20",
+    "@types/react": "18.2.21",
     "@types/webextension-polyfill": "0.10.1",
     "@typescript-eslint/eslint-plugin": "6.4.1",
     "archiver": "6.0.0",
@@ -53,11 +53,11 @@
     "eslint-plugin-solid": "0.12.1",
     "eslint-plugin-tsdoc": "0.2.17",
     "fs-extra": "11.1.1",
-    "glob": "10.3.3",
+    "glob": "10.3.4",
     "imagemin": "8.0.1",
     "imagemin-jpegtran": "7.0.0",
     "imagemin-pngquant": "9.0.2",
-    "prettier": "3.0.1",
+    "prettier": "3.0.2",
     "prettier-config-web-scrobbler": "0.1.0",
     "remark": "14.0.3",
     "remark-cli": "11.0.0",
@@ -66,11 +66,11 @@
     "solid-js": "1.7.11",
     "stylelint-config-web-scrobbler": "0.2.0",
     "tsx": "3.12.7",
-    "typescript": "5.1.6",
+    "typescript": "5.2.2",
     "vite-plugin-conditional-compiler": "0.1.1",
     "vite-plugin-solid": "2.7.0",
     "vite-plugin-static-copy": "0.17.0",
-    "vitest": "0.34.2"
+    "vitest": "0.34.3"
   },
   "engines": {
     "node": ">=18.0.0"
diff --git a/src/connectors/gensokyoradio-dom-inject.ts b/src/connectors/gensokyoradio-dom-inject.ts
new file mode 100644
index 00000000..838d2840
--- /dev/null
+++ b/src/connectors/gensokyoradio-dom-inject.ts
@@ -0,0 +1,31 @@
+/**
+ * This script runs in non-isolated environments (gensokyoradio.net) so that we
+ * have access to the global `audio` player.
+ */
+
+interface GensokyoWindow {
+	audio: HTMLAudioElement;
+}
+
+gensokyoSetupEventListener();
+
+function gensokyoSendUpdateEvent(type: string) {
+	window.postMessage(
+		{
+			sender: 'web-scrobbler',
+			type,
+			isPlaying: (window as unknown as GensokyoWindow).audio.paused
+				? false
+				: true,
+		},
+		'*',
+	);
+}
+
+function gensokyoSetupEventListener() {
+	const audio = (window as unknown as GensokyoWindow).audio;
+
+	for (const e of ['play', 'pause', 'ended', 'emptied', 'suspend']) {
+		audio.addEventListener(e, gensokyoSendUpdateEvent.bind(null, e));
+	}
+}
diff --git a/src/connectors/gensokyoradio.ts b/src/connectors/gensokyoradio.ts
new file mode 100644
index 00000000..fe6b1fa0
--- /dev/null
+++ b/src/connectors/gensokyoradio.ts
@@ -0,0 +1,34 @@
+export {};
+
+let isPlaying = false;
+
+Connector.playerSelector = '#heroMain';
+
+Connector.artistSelector = '#playerArtist';
+
+Connector.trackSelector = '#playerTitle';
+
+Connector.albumSelector = '#playerAlbum';
+
+Connector.albumArtistSelector = '#playerCircle';
+
+Connector.trackArtSelector = '#playerArt';
+
+Connector.timeInfoSelector = '#playerCounter';
+
+Connector.isPlaying = () => isPlaying;
+
+Connector.isTrackArtDefault = (trackArtUrl) => {
+	return (
+		!!trackArtUrl &&
+		/(gr-logo-placeholder|no-albumart)\.png$/i.test(trackArtUrl)
+	);
+};
+
+Connector.onScriptEvent = (event) => {
+	isPlaying = !!event.data.isPlaying;
+
+	Connector.onStateChanged();
+};
+
+Connector.injectScript('connectors/gensokyoradio-dom-inject.js');
diff --git a/src/connectors/stationhead.ts b/src/connectors/stationhead.ts
new file mode 100644
index 00000000..3542b33d
--- /dev/null
+++ b/src/connectors/stationhead.ts
@@ -0,0 +1,10 @@
+export {};
+
+const playerSelector = 
+	'#root>div>div:nth-child(2)>div>div>div>div>div:nth-child(2)>div:nth-child(2)>div';
+
+Connector.useMediaSessionApi();
+
+Connector.playerSelector = playerSelector;
+Connector.trackSelector = `${playerSelector} p`;
+Connector.artistSelector = `${playerSelector}>div>div>div p`;
diff --git a/src/core/background/main.ts b/src/core/background/main.ts
index 8ac71b50..04d61c32 100644
--- a/src/core/background/main.ts
+++ b/src/core/background/main.ts
@@ -29,6 +29,7 @@ import {
 import { CloneableSong } from '@/core/object/song';
 import {
 	clearNowPlaying,
+	showAuthNotification,
 	showNowPlaying,
 	showSongNotRecognized,
 } from '@/util/notifications';
@@ -36,6 +37,7 @@ import ClonedSong from '@/core/object/cloned-song';
 import { openTab } from '@/util/util-browser';
 import { setRegexDefaults } from '@/util/regex';
 import { getSongInfo, scrobble, sendNowPlaying, toggleLove } from './scrobble';
+import scrobbleService from '../object/scrobble-service';
 
 const disabledTabs = BrowserStorage.getStorage(BrowserStorage.DISABLED_TABS);
 
@@ -118,7 +120,7 @@ async function onTabRemoved(tabId: number) {
  * @param activeInfo - Information about the switch of tabs
  */
 async function onTabActivated(
-	activeInfo: browser.Tabs.OnActivatedActiveInfoType
+	activeInfo: browser.Tabs.OnActivatedActiveInfoType,
 ) {
 	await updateTabList(activeInfo.tabId, true);
 }
@@ -134,7 +136,7 @@ async function onTabActivated(
 async function onTabUpdated(
 	tabId: number,
 	changeInfo: browser.Tabs.OnUpdatedChangeInfoType,
-	tab?: browser.Tabs.Tab
+	tab?: browser.Tabs.Tab,
 ) {
 	if (tab?.active && changeInfo.status === 'complete') {
 		await updateTabList(tabId, false);
@@ -161,7 +163,7 @@ async function updateTabList(tabId: number, activated: boolean) {
 			newTabs = [curTab, ...newTabs];
 		} else {
 			newTabs = newTabs.map((active) =>
-				active.tabId === tabId ? curTab : active
+				active.tabId === tabId ? curTab : active,
 			);
 		}
 	}
@@ -182,7 +184,7 @@ async function updateTabList(tabId: number, activated: boolean) {
  */
 async function updateTab(
 	tabId: number | undefined,
-	fn: (tab: ManagerTab) => ManagerTab
+	fn: (tab: ManagerTab) => ManagerTab,
 ): Promise<void> {
 	if (!tabId) {
 		throw new Error('No tabid given');
@@ -250,7 +252,7 @@ async function updateMode(tabId: number | undefined, mode: ControllerModeStr) {
  */
 async function updateState(
 	tabId: number | undefined,
-	song: CloneableSong | null
+	song: CloneableSong | null,
 ) {
 	await updateTab(tabId, (oldTab) => ({
 		tabId: oldTab.tabId,
@@ -308,7 +310,7 @@ setupBackgroundListeners(
 				payload.connector,
 				() => {
 					openTab(sender.tab?.id ?? -1);
-				}
+				},
 			);
 		},
 	}),
@@ -320,7 +322,7 @@ setupBackgroundListeners(
 		type: 'setNowPlaying',
 		fn: (payload, sender) => {
 			return sendNowPlaying(
-				new ClonedSong(payload.song, sender.tab?.id ?? -1)
+				new ClonedSong(payload.song, sender.tab?.id ?? -1),
 			);
 		},
 	}),
@@ -342,7 +344,7 @@ setupBackgroundListeners(
 		type: 'getSongInfo',
 		fn: (payload, sender) => {
 			return getSongInfo(
-				new ClonedSong(payload.song, sender.tab?.id ?? -1)
+				new ClonedSong(payload.song, sender.tab?.id ?? -1),
 			);
 		},
 	}),
@@ -355,7 +357,7 @@ setupBackgroundListeners(
 		fn: (payload, sender) => {
 			return toggleLove(
 				new ClonedSong(payload.song, sender.tab?.id ?? -1),
-				payload.isLoved
+				payload.isLoved,
 			);
 		},
 	}),
@@ -381,7 +383,7 @@ setupBackgroundListeners(
 				payload.connector,
 				() => {
 					openTab(sender.tab?.id ?? -1);
-				}
+				},
 			);
 		},
 	}),
@@ -398,9 +400,38 @@ setupBackgroundListeners(
 				browserPreferredTheme: payload,
 			});
 		},
-	})
+	}),
 );
 
+/**
+ * Replace the extension version stored in local storage by current one.
+ */
+async function updateVersionInStorage() {
+	const storage = BrowserStorage.getStorage(BrowserStorage.CORE);
+	let data = await storage.get();
+	if (!data) {
+		data = {
+			appVersion: '',
+		};
+	}
+
+	data.appVersion = browser.runtime.getManifest().version;
+	await storage.set(data);
+
+	storage.debugLog();
+}
+
+/**
+ * Ask a scrobble service to bind scrobblers.
+ *
+ * @returns true if at least one scrobbler is registered;
+ *          false if no scrobblers are registered
+ */
+async function bindScrobblers() {
+	const boundScrobblers = await scrobbleService.bindAllScrobblers();
+	return boundScrobblers.length > 0;
+}
+
 /**
  * Sets up the starting state of the extension on browser startup/extension install.
  * Storage is used instead of variables, as with Manifest V3 service workers, script state cannot be guaranteed.
@@ -411,6 +442,13 @@ function startupFunc() {
 	disabledTabs.set({});
 
 	setRegexDefaults();
+	updateVersionInStorage();
+	bindScrobblers().then((bound) => {
+		if (!bound) {
+			console.warn('No scrobblers are bound');
+			showAuthNotification();
+		}
+	});
 
 	browser.contextMenus?.create({
 		id: contextMenus.ENABLE_CONNECTOR,
diff --git a/src/core/connectors.ts b/src/core/connectors.ts
index 41f919ae..b90bb4bf 100644
--- a/src/core/connectors.ts
+++ b/src/core/connectors.ts
@@ -1926,7 +1926,7 @@ export default <ConnectorMeta[]>[
 	},
 	{
 		label: 'Beatbump',
-		matches: ['*://beatbump.ml/*'],
+		matches: ['*://beatbump.io/*'],
 		js: 'beatbump.js',
 		id: 'beatbump',
 	},
@@ -2112,4 +2112,16 @@ export default <ConnectorMeta[]>[
 		js: 'trackerhub.js',
 		id: 'trackerhub',
 	},
+	{
+		label: 'Gensokyo Radio',
+		matches: ['*://gensokyoradio.net/*'],
+		js: 'gensokyoradio.js',
+		id: 'gensokyoradio',
+	},
+	{
+		label: 'Stationhead',
+		matches: ['*://*stationhead.com/*'],
+		js: 'stationhead.js',
+		id: 'stationhead',
+	},
 ];
diff --git a/src/ui/options/main.tsx b/src/ui/options/main.tsx
index 9b477304..9c0d4eee 100644
--- a/src/ui/options/main.tsx
+++ b/src/ui/options/main.tsx
@@ -12,6 +12,8 @@ import {
 	ModalType,
 	NavigatorNavigationButton,
 	aboutItem,
+	accountItem,
+	connectorOverrideOptionsItem,
 	settings,
 	showSomeLoveItem,
 } from './components/navigator';
@@ -26,11 +28,26 @@ function contextMenuQuery() {
 	return window.matchMedia('(max-width: 700px)').matches;
 }
 
-// Show some love as default except safari, where we don't want it shown because apple
-let defaultSetting = aboutItem;
-// #v-ifndef VITE_SAFARI
-defaultSetting = showSomeLoveItem;
-// #v-endif
+function getDefaultSetting(): NavigatorNavigationButton {
+	// if a page override is selected, return that page
+	const pageTitle = new URLSearchParams(window.location.search).get('p');
+	switch (pageTitle) {
+		case 'accounts':
+			return accountItem;
+		case 'connectors':
+			return connectorOverrideOptionsItem;
+	}
+
+	// in non-safari, go to show some love page
+	// #v-ifndef VITE_SAFARI
+	return showSomeLoveItem;
+	// #v-endif
+
+	// We actually reach this in safari, return about page
+	return aboutItem;
+}
+
+const defaultSetting = getDefaultSetting();
 
 /**
  * Preferences component, with a sidebar and several different options and info pages
@@ -59,7 +76,7 @@ function Options() {
 	onCleanup(() => document.removeEventListener('click', onclick));
 
 	const [shouldShowContextMenu, setShouldShowContextMenu] = createSignal(
-		contextMenuQuery()
+		contextMenuQuery(),
 	);
 	const resizeListener = () => setShouldShowContextMenu(contextMenuQuery());
 	window.addEventListener('resize', resizeListener);
diff --git a/src/ui/popup/disabled.tsx b/src/ui/popup/disabled.tsx
index 6a03ad8a..81ff9513 100644
--- a/src/ui/popup/disabled.tsx
+++ b/src/ui/popup/disabled.tsx
@@ -16,7 +16,9 @@ export default function Disabled() {
 			<h1>{t('disabledSiteHeader')}</h1>
 			<p>{t('disabledSiteDesc')}</p>
 			<PopupAnchor
-				href={browser.runtime.getURL('src/ui/options/index.html')}
+				href={browser.runtime.getURL(
+					'src/ui/options/index.html?p=connectors',
+				)}
 				class={`${optionComponentStyles.linkButton} ${styles.centered}`}
 			>
 				<Settings />
diff --git a/src/ui/popup/err.tsx b/src/ui/popup/err.tsx
index 21418525..c4695105 100644
--- a/src/ui/popup/err.tsx
+++ b/src/ui/popup/err.tsx
@@ -18,7 +18,9 @@ export default function Err() {
 				<TPopupAnchor messageName="serviceErrorDesc" />
 			</p>
 			<PopupAnchor
-				href={browser.runtime.getURL('src/ui/options/index.html')}
+				href={browser.runtime.getURL(
+					'src/ui/options/index.html?p=accounts',
+				)}
 				class={`${optionComponentStyles.linkButton} ${styles.centered}`}
 			>
 				<Settings />
diff --git a/src/util/notifications.ts b/src/util/notifications.ts
index 48b8fd28..1e521b78 100644
--- a/src/util/notifications.ts
+++ b/src/util/notifications.ts
@@ -6,6 +6,7 @@ import * as Options from '@/core/storage/options';
 import { ConnectorMeta } from '@/core/connectors';
 import { Scrobbler } from '@/core/object/scrobble-service';
 import { debugLog } from '@/core/content/util';
+import * as BrowserStorage from '@/core/storage/browser-storage';
 
 /**
  * Notification service.
@@ -43,6 +44,9 @@ let notificationTimeoutId: NodeJS.Timeout | null = null;
  * @returns Check result
  */
 async function isAvailable() {
+	if (!browser?.notifications) {
+		return false;
+	}
 	// #v-ifdef VITE_CHROME
 	const platform = await getPlatformName();
 	if (platform === 'mac') {
@@ -74,7 +78,7 @@ async function isAllowed(connector: ConnectorMeta) {
  * @param notificationId - Notification ID
  * @param callback - Function that will be called on notification click
  */
-function addOnClickedListener(notificationId: number, callback: () => void) {
+function addOnClickedListener(notificationId: string, callback: () => void) {
 	clickListeners[notificationId] = callback;
 }
 
@@ -112,7 +116,7 @@ interface ProcessedNotificationOptions extends BaseNotificationOptions {
  */
 async function showNotification(
 	options: BaseNotificationOptions,
-	onClick: (() => void) | null
+	onClick: (() => void) | null,
 ) {
 	if (!(await isAvailable())) {
 		throw new Error('Notifications are not available');
@@ -131,7 +135,7 @@ async function showNotification(
 	try {
 		notificationId = await browser.notifications?.create(
 			'',
-			processedOptions
+			processedOptions,
 		);
 	} catch (err) {
 		// Use default track art and try again
@@ -142,12 +146,12 @@ async function showNotification(
 		processedOptions.iconUrl = defaultTrackArtUrl;
 		notificationId = await browser.notifications?.create(
 			'',
-			processedOptions
+			processedOptions,
 		);
 	}
 
 	if (typeof onClick === 'function') {
-		addOnClickedListener(parseInt(notificationId), onClick);
+		addOnClickedListener(notificationId, onClick);
 	}
 
 	return notificationId;
@@ -162,7 +166,7 @@ async function showNotification(
 export async function showNowPlaying(
 	song: BaseSong,
 	connector: ConnectorMeta,
-	onClick: () => void
+	onClick: () => void,
 ): Promise<void> {
 	if (!(await isAllowed(connector))) {
 		return;
@@ -186,7 +190,7 @@ export async function showNowPlaying(
 	if (userPlayCount) {
 		const userPlayCountStr = browser.i18n.getMessage(
 			'infoYourScrobbles',
-			userPlayCount.toString()
+			userPlayCount.toString(),
 		);
 		message = `${message ?? 'null'}\n${userPlayCountStr}`;
 	}
@@ -235,7 +239,7 @@ export function clearNowPlaying(song: BaseSong): void {
  */
 export function showError(
 	message: string,
-	onClick: (() => void) | null = null
+	onClick: (() => void) | null = null,
 ): void {
 	const title = browser.i18n.getMessage('notificationAuthError');
 	const options = { title, message };
@@ -249,11 +253,11 @@ export function showError(
  */
 export function showSignInError(
 	scrobbler: Scrobbler,
-	onClick: () => void
+	onClick: () => void,
 ): void {
 	const errorMessage = browser.i18n.getMessage(
 		'notificationUnableSignIn',
-		scrobbler.getLabel()
+		scrobbler.getLabel(),
 	);
 	showError(errorMessage, onClick);
 }
@@ -267,12 +271,12 @@ export function showSignInError(
 export async function showSongNotRecognized(
 	song: BaseSong,
 	connector: ConnectorMeta,
-	onClick: () => void
+	onClick: () => void,
 ): Promise<void> {
 	if (
 		!(await Options.getOption(
 			Options.USE_UNRECOGNIZED_SONG_NOTIFICATIONS,
-			connector.id
+			connector.id,
 		))
 	) {
 		return;
@@ -284,21 +288,81 @@ export async function showSongNotRecognized(
 		message: browser.i18n.getMessage('notificationNotRecognizedText'),
 	};
 
-	const notificationId = await showNotification(options, onClick);
-	song.metadata.notificationId = notificationId;
+	try {
+		const notificationId = await showNotification(options, onClick);
+		song.metadata.notificationId = notificationId;
+	} catch (err) {
+		debugLog('Unable to show song not recognized notification: ', 'warn');
+		debugLog(err, 'warn');
+	}
+}
+
+/**
+ * How many times to show auth notification.
+ */
+const authNotificationDisplayCount = 3;
+
+/**
+ * Storage for auth notification display count.
+ */
+const notificationStorage = BrowserStorage.getStorage(
+	BrowserStorage.NOTIFICATIONS,
+);
+
+/**
+ * Check if auth notification is allowed.
+ * @returns Check result
+ */
+async function isAuthNotificationAllowed(): Promise<boolean> {
+	const displayCount =
+		(await notificationStorage.get())?.authDisplayCount || 0;
+	return displayCount < authNotificationDisplayCount;
+}
+
+/**
+ * Update internal counter of displayed auth notifications.
+ */
+async function updateAuthDisplayCount() {
+	let data = await notificationStorage.get();
+	if (!data) {
+		data = {
+			authDisplayCount: 0,
+		};
+	}
+
+	data.authDisplayCount = data.authDisplayCount + 1;
+	await notificationStorage.set(data);
 }
 
 /**
  * Show auth notification.
- * @param onClick - Function that will be called on notification click
  */
-export async function showAuthNotification(onClick: () => void): Promise<void> {
+export async function showAuthNotification(): Promise<void> {
+	if (!(await isAuthNotificationAllowed())) {
+		return;
+	}
+
 	const options = {
 		title: browser.i18n.getMessage('notificationConnectAccounts'),
 		message: browser.i18n.getMessage('notificationConnectAccountsText'),
 	};
 
-	await showNotification(options, onClick);
+	try {
+		await showNotification(options, () => {
+			browser.tabs.create({
+				url: browser.runtime.getURL(
+					'src/ui/options/index.html?p=accounts',
+				),
+			});
+		});
+	} catch (err) {
+		debugLog('Unable to show auth notification: ', 'warn');
+		debugLog(err, 'warn');
+		browser.tabs.create({
+			url: browser.runtime.getURL('src/ui/options/index.html?p=accounts'),
+		});
+	}
+	void updateAuthDisplayCount();
 }
 
 /**


Test patch:
diff --git a/vitest.config.ts b/vitest.config.ts
index 1d9c25e3..977024a1 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -9,4 +9,7 @@ export default {
 		},
 	},
 	plugins: [ConditionalCompile()],
+	test: {
+		testTimeout: 10000,
+	},
 };