Repository: web-scrobbler/web-scrobbler
Pull request #4117 (2023-09-20 11:26:09)

Improve bracket matching
New bracket just dropped.
Also makes the pattern matching of brackets less greedy.


Patch:
diff --git a/manifest.config.ts b/manifest.config.ts
index c5bb17dc..1f825074 100644
--- a/manifest.config.ts
+++ b/manifest.config.ts
@@ -28,6 +28,10 @@ export const common: Manifest.WebExtensionManifest = {
 			resources: ['connectors/*'],
 			matches: ['<all_urls>'],
 		},
+		{
+			resources: ['icons/*'],
+			matches: ['<all_urls>'],
+		},
 	],
 
 	options_ui: {
diff --git a/src/_locales/de/messages.json b/src/_locales/de/messages.json
index 58185861..49a85a27 100644
--- a/src/_locales/de/messages.json
+++ b/src/_locales/de/messages.json
@@ -7,6 +7,10 @@
         "message": "Allgemeines",
         "description": "'General' section"
     },
+    "optionsPermissionsLabel": {
+        "message": "Du hast Web Scrobbler nicht die Berechtigung erteilt, auf alle Websites zuzugreifen. Daher musst du jedes Mal auf das Icon dieser Erweiterung klicken, um mit dem Scrobbeln zu beginnen. Klicke hier, um die Berechtigung zu erteilen. Nimm zur Kenntnis, dass in Safari die Berechtigung nicht automatisch erteilt wird, aber beim nächsten Besuch einer unterstützten Website hast du die Möglichkeit, auf \"Always Allow on Every Website…\" zu klicken, um die Berechtigung zu erteilen.",
+        "description": "Label for permissions button"
+    },
     "optionUseNotifications": {
         "message": "Verwende Benachrichtigungen für laufenden Titel",
         "description": "Option label"
@@ -23,6 +27,18 @@
         "message": "Benachrichtigung anzeigen wenn Titel nicht von der Erweiterung erkannt wird",
         "description": "Option title"
     },
+    "optionScrobblePodcasts": {
+        "message": "Podcasts scrobbeln",
+        "description": "Option label"
+    },
+    "optionScrobblePodcastsTitle": {
+        "message": "Podcast-Episoden scrobbeln",
+        "description": "Option title"
+    },
+    "optionsScrobbleBehavior": {
+        "message": "Scrobble-Verhalten",
+        "description": "'Scrobble behavior' section"
+    },
     "optionForceRecognize": {
         "message": "Titelerkennung erzwingen",
         "description": "Option label"
@@ -31,6 +47,22 @@
         "message": "Erkennen von unbekannten Titeln erzwingen",
         "description": "Option title"
     },
+    "optionScrobbleRecognizedTracks": {
+        "message": "Scrobble erkannt oder bearbeiteter Titel (Standard)",
+        "description": "Option label"
+    },
+    "optionScrobbleRecognizedTracksTitle": {
+        "message": "Scrobble nur Titel die erkannt oder bearbeitet wurden",
+        "description": "Option title"
+    },
+    "optionScrobbleEditedTracksOnly": {
+        "message": "Scrobble nur bearbeitete Titel",
+        "description": "Option label"
+    },
+    "optionScrobbleEditedTracksOnlyTitle": {
+        "message": "Scrobble Titel nur, falls sie bearbeitet wurden",
+        "description": "Option title"
+    },
     "optionsHidden": {
         "message": "Versteckt",
         "description": "'Hidden' section"
@@ -47,6 +79,10 @@
         "message": "Wenn du beide Optionen deaktivierst, wird die Erweiterung YouTube videos aus allen Kategorien scrobblen.",
         "description": "Description of YouTube options"
     },
+    "optionPercentDesc": {
+        "message": "Nimm zur Kenntnis, dass ein Titel unabhängig von dieser Einstellung trotzdem gescrobblet wird, falls er länger als 4 Minuten gespielt wurde.",
+        "description": "Description of scrobble percent option"
+    },
     "optionYtMusicOnly": {
         "message": "Videos aus der Kategorie \"Musik\" scrobblen",
         "description": "Option title"
@@ -63,6 +99,30 @@
         "message": "Ignoriere Videos die nicht in Kategorie Unterhaltung sind",
         "description": "Option title"
     },
+    "optionYtGetTrackInfoFromYtMusic": {
+        "message": "Verwende YouTube Music um Titelinformationen zu ermitteln",
+        "description": "Option label"
+    },
+    "optionYtGetTrackInfoFromYtMusicTitle": {
+        "message": "Verwende YouTube Music um Titelinformationen zu ermitteln; falls abgewählt, werden standardmäßig Beschreibung und Titel des Videos benutzt",
+        "description": "Option title"
+    },
+    "optionYtMusicRecognisedOnly": {
+        "message": "Scrobble nur Videos, die von YouTube Music als Musik erkannt werden",
+        "description": "Option label"
+    },
+    "optionYtMusicRecognisedOnlyTitle": {
+        "message": "Ignoriere Videos, die nicht von YouTube Music als Musik erkannt werden",
+        "description": "Option title"
+    },
+    "optionTheme": {
+        "message": "Theme",
+        "description": "Option label"
+    },
+    "optionThemeTitle": {
+        "message": "Wähle das Theme der Erweiterung",
+        "description": "Option title"
+    },
     "optionThemeLight": {
         "message": "Licht",
         "description": "Option label"
@@ -115,10 +175,22 @@
         "message": "API URL personalisieren",
         "description": "Input label"
     },
+    "accountsUserToken": {
+        "message": "Token",
+        "description": "Input label"
+    },
+    "accountsUserTokenPlaceholder": {
+        "message": "Individuelles Token",
+        "description": "Input label"
+    },
     "optionsOptions": {
         "message": "Einstellungen",
         "description": "'Options' section"
     },
+    "optionsEdits": {
+        "message": "Bearbeitungen",
+        "description": "'Edits' section"
+    },
     "optionsEditedTracks": {
         "message": "Bearbeitete Title",
         "description": "'Edited tracks' header"
@@ -131,6 +203,18 @@
         "message": "$1 bearbeitete Titel",
         "description": "'Edited tracks' header"
     },
+    "optionsRegexEdits": {
+        "message": "Regex-/Massenbearbeitungen",
+        "description": "'Regex/Bulk edits' header"
+    },
+    "optionsRegexEditsDesc": {
+        "message": "Diese Erweiterung speichert Regex-/Massenbearbeitungen.",
+        "description": "Description of regex/bulk edits"
+    },
+    "optionsRegexEditsPopupTitle": {
+        "message": "$1 Regex-/Massenbearbeitungen",
+        "description": "'Regex/Bulk edits' header"
+    },
     "optionsViewEdited": {
         "message": "Ansehen",
         "description": "Button to view edited tracks"
@@ -143,6 +227,10 @@
         "message": "Importieren",
         "description": "Button to import edited tracks"
     },
+    "optionsConnectors": {
+        "message": "Verbinder",
+        "description": "'Connectors' section"
+    },
     "optionsEnableDisableHint": {
         "message": "Klick auf den Kontrollkasten um Verbinder zu aktivieren/deaktivieren.",
         "description": "Enable/disable hint"
@@ -159,6 +247,10 @@
         "message": "An/Ausschalten",
         "description": "Label of toggle button"
     },
+    "optionsLoadingConnectorOptions": {
+        "message": "Lade Verbinder-Optionen...",
+        "description": "Loading connector options message"
+    },
     "learnMoreLabel": {
         "message": "Mehr erfahren",
         "description": "Learn more label"
@@ -280,7 +372,7 @@
         "description": "'About' section"
     },
     "aboutChangelog": {
-        "message": "Der Liste der Änderungen in der aktuellen Version ist <a id='latest-release' target='_blank' href='#'>hier</a> verfügbar. Für die komplette Liste aller Änderungen schau auf <a target='_blank' href='https://github.com/web-scrobbler/web-scrobbler/releases'>dieser Seite</a>.",
+        "message": "Das Änderungsprotokoll der aktuellen Version ist <a id='latest-release' target='_blank' href='#'>hier</a> verfügbar. Besuche für das vollständige Änderungsprotokoll <a target='_blank' href='https://github.com/web-scrobbler/web-scrobbler/releases'>diese Seite</a>.",
         "description": "'About' section text"
     },
     "aboutExtensionDesc": {
@@ -295,10 +387,18 @@
         "message": "Du kannst die Liste der Mitwirkenden <a href=\"https://github.com/web-scrobbler/web-scrobbler/graphs/contributors\">direkt auf Github</a> sehen",
         "description": "'About' section text"
     },
+    "contributorsContribute": {
+        "message": "Möchtest du einen Beitrag leisten? Finde alle Möglichkeiten an diesem Projekt mitzuwirken auf <a href=\"https://web-scrobbler.com#contributing\" target=\"_blank\" rel=\"noopener noreferrer\">unserer Website</a>.",
+        "description": "'About' section text"
+    },
     "showSomeLoveTitle": {
         "message": "Zeig deine Unterstützung",
         "description": "'Show some love' section"
     },
+    "showSomeLoveFundingCampaign": {
+        "message": "Mit der Zeit wurde ein großer Bestand an Aufgaben im Web-Scrobbler-Repository aufgebaut. Von Mitte August 2023 bis Mitte März 2024 steht einer unserer Hauptentwickler zur Verfügung, um mit geringeren Bezügen Vollzeit an diesem Projekt zu arbeiten. Für alle $$1500, die wir aufbringen, können wir ungefähr einen Monat Vollzeitarbeit finanzieren bis zu einem Gesamtbetrag von etwa $$9-10k. Wenn du diese Kampagne unterstützen möchtest und uns hilfst, unseren Bearbeitungsrückstand abzuarbeiten und so Web Scrobbler noch besser zu machen, benutze den Button unten. Wir verpflichten uns weiterhin, Web Scrobbler mit all seinen Features für immer kostenlos zu halten. Bitte hilf uns, diese Botschaft zu verbreiten.",
+        "description": "Temporary funding campaign"
+    },
     "showSomeLoveText1": {
         "message": "Anders als andere Erweiterungen enthält diese keine blinkenden Banner oder hinterhältige Werbung.",
         "description": ""
@@ -307,6 +407,10 @@
         "message": "Die Browser-Erweiterung ist kostenlos und wird immer zwar gebührenfrei sein. Wenn Sie aber ihre Entwicklung unterstützen wollen, können Sie den Knöpf unter klicken. ",
         "description": ""
     },
+    "donationButtonLabel": {
+        "message": "Unterstütze uns auf OpenCollective",
+        "description": "The donation button label"
+    },
     "customPatterns": {
         "message": "URL-Muster",
         "description": "'URL patterns' section"
@@ -391,6 +495,10 @@
         "message": "Aufgrund einer ExtensionSettings-Richtlinie können einige durch die Erweiterung unterstützten Google-Webseiten (z.B. Google Play Music, YouTube) gesperrt angezeigt werden. ",
         "description": "The description of 'unsupported' popup"
     },
+    "unsupportedWebsiteUpdateNote": {
+        "message": "Falls die Erweiterung kürzlich aktualisiert wurde, können Websites als nicht unterstützt angezeigt werden, bis du die Seite neu geladen hast.",
+        "description": "The description of 'unsupported' popup"
+    },
     "infoLove": {
         "message": "Lieblingstitel",
         "description": "Label of love/unlove button"
@@ -407,6 +515,14 @@
         "message": "Du kannst diesen Titel nicht bearbeiten",
         "description": "Title of edit button"
     },
+    "infoEditTitleShort": {
+        "message": "Bearbeiten",
+        "description": "Title of edit button, should be short for mobile button label"
+    },
+    "infoEditUnableTitleShort": {
+        "message": "Bearbeitung nicht möglich",
+        "description": "Title of edit button, should be short for mobile button label"
+    },
     "infoSwapTitle": {
         "message": "Tausche Titelname und Künstler aus",
         "description": "Title of swap button"
@@ -415,6 +531,26 @@
         "message": "Sie dürfen nicht leere Musiktitel scrobbeln",
         "description": "Title of swap button"
     },
+    "infoSwapTitleShort": {
+        "message": "Titel und Künstler tauschen",
+        "description": "Title of swap button, should be short for mobile button label"
+    },
+    "infoSwapUnableTitleShort": {
+        "message": "Info kann nicht getauscht werden",
+        "description": "Title of swap button, should be short for mobile button label"
+    },
+    "infoRegexTitle": {
+        "message": "Regex-/Massenbearbeitungs-Modus",
+        "description": "Title of regex/bulk edit button"
+    },
+    "infoRegexTitleShort": {
+        "message": "Regex-/Massenbearbeitung",
+        "description": "Title of regex/bulk edit button, should be short for mobile button label"
+    },
+    "infoEditedWarning": {
+        "message": "Dieser Titel wurde bearbeitet. Regex-Bearbeitungen werden nicht auf bearbeitete Titel angewendet; diese Vorschau kann ungenau sein.",
+        "description": "Warning message to user if bulk editing on a previously edited track"
+    },
     "infoSubmitTitle": {
         "message": "Änderungen einsenden",
         "description": "Title of submit button"
@@ -423,6 +559,14 @@
         "message": "Sie dürfen nicht leere Musiktitel scrobbeln",
         "description": "Title of submit button"
     },
+    "infoSubmitTitleShort": {
+        "message": "Übermitteln",
+        "description": "Title of submit button, should be short for mobile button label"
+    },
+    "infoSubmitUnableTitleShort": {
+        "message": "Übermittlung nicht möglich",
+        "description": "Title of submit button, should be short for mobile button label"
+    },
     "infoRevertTitle": {
         "message": "Änderungen zurücksetzen",
         "description": "Title of revert button"
@@ -431,6 +575,14 @@
         "message": "Du kannst einen bereits gescrobbleten Titel nicht zurücksetzen",
         "description": "Title of revert button"
     },
+    "infoRevertTitleShort": {
+        "message": "Rückgängig machen",
+        "description": "Title of revert button, should be short for mobile button label"
+    },
+    "infoRevertUnableTitleShort": {
+        "message": "Rückgängig machen nicht möglich",
+        "description": "Title of revert button, should be short for mobile button label"
+    },
     "infoSkipTitle": {
         "message": "Den aktuellen Titel nicht scrobblen",
         "description": "Title of skip button"
@@ -443,6 +595,18 @@
         "message": "Sie haben diesen Titel geskippt",
         "description": "Title of unskip button"
     },
+    "infoSkipTitleShort": {
+        "message": "Überspringen",
+        "description": "Title of skip button, should be short for mobile button label"
+    },
+    "infoSkipUnableTitleShort": {
+        "message": "Überspringen nicht möglich",
+        "description": "Title of skip button, should be short for mobile button label"
+    },
+    "infoSkippedTitleShort": {
+        "message": "Übersprungen",
+        "description": "Title of unskip button, should be short for mobile button label"
+    },
     "infoOpenAlbumArt": {
         "message": "Albumcover in neuem Tab öffnen",
         "description": "Title of album image"
@@ -463,6 +627,34 @@
         "message": "Albumkünstler (optional)",
         "description": "Placeholder of album artist input"
     },
+    "infoSearchLabel": {
+        "message": "Suche",
+        "description": "Label of regex edit search input"
+    },
+    "infoReplaceLabel": {
+        "message": "Ersetze",
+        "description": "Label of regex edit replace input"
+    },
+    "infoPreviewLabel": {
+        "message": "Vorschau",
+        "description": "Label of regex edit preview output"
+    },
+    "infoArtistLabel": {
+        "message": "Künstler",
+        "description": "Label of artist input row"
+    },
+    "infoTrackLabel": {
+        "message": "Titel",
+        "description": "Label of track input row"
+    },
+    "infoAlbumLabel": {
+        "message": "Album",
+        "description": "Label of album input row"
+    },
+    "infoAlbumArtistLabel": {
+        "message": "Albumkünstler",
+        "description": "Label of album artist input row"
+    },
     "infoViewArtistPage": {
         "message": "Künstlerseite ansehen: $1",
         "description": "Placeholder of album input"
@@ -475,6 +667,10 @@
         "message": "Titelseite ansehen: $1",
         "description": "Placeholder of album input"
     },
+    "infoYourScrobbles": {
+        "message": "Deine Scrobbles: $1",
+        "description": "Label for user play count"
+    },
     "pageActionBase": {
         "message": "Diese Seite wird für Scrobbling unterstützt",
         "description": "`base` page action title"
@@ -575,4 +771,4 @@
         "message": "Scrobble Musik überall im Netz!",
         "description": "Description of the extension"
     }
-}
+}
\ No newline at end of file
diff --git a/src/_locales/de/privacy.md b/src/_locales/de/privacy.md
index de1d71f6..d8751a97 100644
--- a/src/_locales/de/privacy.md
+++ b/src/_locales/de/privacy.md
@@ -1,29 +1,29 @@
-# Privacy policy
+# Datenschutzrichtlinie
 
-This privacy policy explains what data we collect and store; how we use and disclose this data.
+Diese Datenschutzrichtlinie erklärt, welche Daten wir erheben und speichern, wie wir sie verwenden und mitteilen.
 
-**Important note**: This document is applicable to Web Scrobbler v2.40.0 and newer.
+**Wichtiger Hinweis**: Dieses Dokument ist anwendbar auf Web Scrobbler v2.40.0 und neuer.
 
-## What data is collected?
+## Welche Daten werden erhoben?
 
-The extension does not collect any of your personal data.
+Diese Erweiterung sammelt keine deiner personenbezogenen Daten.
 
-## What data is stored by the extension?
+## Welche Daten werden von der Erweiterung gespeichert?
 
-Your settings and credentials are stored within your browser. The extension stores:
+Deine Einstellungen und Zugangsdaten werden in deinem Browser gespeichert. Die Erweiterung speichert:
 
- - Your credentials of scrobble services
- - Your personal Web Scrobbler settings
- - Your edited tracks
- - Your custom URL patterns for connectors
- - System information of the extension
+ - Deine Zugangsdaten für Scrobble-Dienste
+ - Deine persönlichen Einstellungen für Web Scrobbler
+ - Deine bearbeiteten Titel
+ - Deine individuellen URL-Muster für Verbinder
+ - Systeminformationen der Erweiterung
 
-Your credentials are used to communicate with scrobble services only.
+Deine Zugangsdaten werden nur zur Kommunikation mit dem Scrobble-Dienst verwendet.
 
-All data is used by the extension internally; we don't send this data to 3rd-party services.
+Alle Daten werden innerhalb der Erweiterung verarbeitet; wir senden diese Daten nicht an Drittanbieter. 
 
-## Why does the extension access to all websites?
+## Warum greift die Erweiterung auf alle Websites zu?
 
-The extension supports several self-hosted services (e.g. Funkwhale), which are supposed to have user-defined URLs. The extension could not know these URLs, that's why access to all websites is required.
+Die Erweiterung unterstützt etliche selbstgehostete Dienste (z.B. Funkwhale), die üblicherweise benutzerdefinierte URLs haben. Die Erweiterung kann diese URLs nicht kennen; daher ist der Zugriff auf alle Websites erforderlich.
 
-Also, it asks accessing for user convenience only. Users will not be prompted to confirm access to new websites added in new extension versions.
+Darüber hinaus erbittet sie den Zugriff nur noch wegen der Nutzerfreundlichkeit. Nutzer werden nicht bei Websites, die in neueren Versionen der Erweiterung hinzugefügt werden, erneut zur Zustimmung aufgefordert.
diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index 03c3fa11..68702adf 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -31,6 +31,14 @@
         "message": "Show notification if track is not recognized by the extension",
         "description": "Option title"
     },
+    "optionUseInfobox": {
+        "message": "Show infobox",
+        "description": "Option label"
+    },
+    "optionUseInfoboxTitle": {
+        "message": "Show web scrobbler infobox on page for supported websites",
+        "description": "Option title"
+    },
     "optionScrobblePodcasts": {
         "message": "Scrobble podcasts",
         "description": "Option label"
@@ -745,31 +753,31 @@
     },
     "pageActionBase": {
         "message": "This site is supported for scrobbling",
-        "description": "`base` page action title"
+        "description": "`base` page action title, also used for infobox"
     },
     "pageActionLoading": {
         "message": "Looking up $1",
-        "description": "`loading` page action title"
+        "description": "`loading` page action title, also used for infobox"
     },
     "pageActionPlaying": {
         "message": "Now playing $1",
-        "description": "`playing` page action title"
+        "description": "`playing` page action title, also used for infobox"
     },
     "pageActionScrobbled": {
         "message": "Scrobbled $1",
-        "description": "`scrobbled` page action title"
+        "description": "`scrobbled` page action title, also used for infobox"
     },
     "pageActionSkipped": {
         "message": "Skipped $1",
-        "description": "`skipped` page action title"
+        "description": "`skipped` page action title, also used for infobox"
     },
     "pageActionIgnored": {
         "message": "Ignored $1",
-        "description": "`ignored` page action title"
+        "description": "`ignored` page action title, also used for infobox"
     },
     "pageActionDisabled": {
         "message": "This site is supported for scrobbling, but you disabled it",
-        "description": "`disabled` page action title"
+        "description": "`disabled` page action title, also used for infobox"
     },
     "pageActionUnknown": {
         "message": "The track is not recognized. Click to submit correct info.",
@@ -838,5 +846,17 @@
     "hotkeyUnloveSong": {
         "message": "Unlove now playing song",
         "description": "Hotkey label"
+    },
+    "infoBoxStateDisallowed": {
+        "message":  "Scrobbling is not allowed. Click web scrobbler extension icon for more information.",
+        "description": "Infobox text when an option is disallowing scrobbling"
+    },
+    "infoBoxStateError": {
+        "message": "A service error occurred. Click web scrobbler extension icon for more information.",
+        "description": "Infobox text when an error occured"
+    },
+    "infoBoxStateUnknown": {
+        "message": "The track is not recognized. Click web scrobbler extension icon to correct info.",
+        "description": "Infobox track is not recognized"
     }
-}
+}
\ No newline at end of file
diff --git a/src/_locales/en_BR/messages.json b/src/_locales/en_BR/messages.json
index fd0c3092..7cc092d8 100644
--- a/src/_locales/en_BR/messages.json
+++ b/src/_locales/en_BR/messages.json
@@ -774,5 +774,17 @@
     "extDescription": {
         "message": "Scrobble music all around the web!",
         "description": "Description of the extension"
+    },
+    "infoBoxStateDisallowed": {
+        "message":  "Scrobbling is not allowed. Click web scrobbler extension icon for more information.",
+        "description": "Infobox text when an option is disallowing scrobbling"
+    },
+    "infoBoxStateError": {
+        "message": "A service error occurred. Click web scrobbler extension icon for more information.",
+        "description": "Infobox text when an error occured"
+    },
+    "infoBoxStateUnknown": {
+        "message": "The track is not recognized. Click web scrobbler extension icon to correct info.",
+        "description": "Infobox track is not recognized"
     }
 }
\ No newline at end of file
diff --git a/src/_locales/en_GB/messages.json b/src/_locales/en_GB/messages.json
index fd0c3092..246270e0 100644
--- a/src/_locales/en_GB/messages.json
+++ b/src/_locales/en_GB/messages.json
@@ -677,35 +677,35 @@
     },
     "pageActionBase": {
         "message": "This site is supported for scrobbling",
-        "description": "`base` page action title"
+        "description": "`base` page action title, also used for infobox"
     },
     "pageActionLoading": {
         "message": "Looking up $1",
-        "description": "`loading` page action title"
+        "description": "`loading` page action title, also used for infobox"
     },
     "pageActionPlaying": {
         "message": "Now playing $1",
-        "description": "`playing` page action title"
+        "description": "`playing` page action title, also used for infobox"
     },
     "pageActionScrobbled": {
         "message": "Scrobbled $1",
-        "description": "`scrobbled` page action title"
+        "description": "`scrobbled` page action title, also used for infobox"
     },
     "pageActionSkipped": {
         "message": "Skipped $1",
-        "description": "`skipped` page action title"
+        "description": "`skipped` page action title, also used for infobox"
     },
     "pageActionIgnored": {
         "message": "Ignored $1",
-        "description": "`ignored` page action title"
+        "description": "`ignored` page action title, also used for infobox"
     },
     "pageActionDisabled": {
         "message": "This site is supported for scrobbling, but you disabled it",
-        "description": "`disabled` page action title"
+        "description": "`disabled` page action title, also used for infobox"
     },
     "pageActionUnknown": {
         "message": "The track is not recognized. Click to submit correct info.",
-        "description": "`unknown` page action title"
+        "description": "`unknown` page action title, also used for infobox"
     },
     "pageActionError": {
         "message": "A service error occurred. Click for more information.",
@@ -774,5 +774,17 @@
     "extDescription": {
         "message": "Scrobble music all around the web!",
         "description": "Description of the extension"
+    },
+    "infoBoxStateDisallowed": {
+        "message":  "Scrobbling is not allowed. Click web scrobbler extension icon for more information.",
+        "description": "Infobox text when an option is disallowing scrobbling"
+    },
+    "infoBoxStateError": {
+        "message": "A service error occurred. Click web scrobbler extension icon for more information.",
+        "description": "Infobox text when an error occured"
+    },
+    "infoBoxStateUnknown": {
+        "message": "The track is not recognized. Click web scrobbler extension icon to correct info.",
+        "description": "Infobox track is not recognized"
     }
 }
\ No newline at end of file
diff --git a/src/_locales/en_US/messages.json b/src/_locales/en_US/messages.json
index fd0c3092..079a6445 100644
--- a/src/_locales/en_US/messages.json
+++ b/src/_locales/en_US/messages.json
@@ -677,35 +677,35 @@
     },
     "pageActionBase": {
         "message": "This site is supported for scrobbling",
-        "description": "`base` page action title"
+        "description": "`base` page action title, also used for infobox"
     },
     "pageActionLoading": {
         "message": "Looking up $1",
-        "description": "`loading` page action title"
+        "description": "`loading` page action title, also used for infobox"
     },
     "pageActionPlaying": {
         "message": "Now playing $1",
-        "description": "`playing` page action title"
+        "description": "`playing` page action title, also used for infobox"
     },
     "pageActionScrobbled": {
         "message": "Scrobbled $1",
-        "description": "`scrobbled` page action title"
+        "description": "`scrobbled` page action title, also used for infobox"
     },
     "pageActionSkipped": {
         "message": "Skipped $1",
-        "description": "`skipped` page action title"
+        "description": "`skipped` page action title, also used for infobox"
     },
     "pageActionIgnored": {
         "message": "Ignored $1",
-        "description": "`ignored` page action title"
+        "description": "`ignored` page action title, also used for infobox"
     },
     "pageActionDisabled": {
         "message": "This site is supported for scrobbling, but you disabled it",
-        "description": "`disabled` page action title"
+        "description": "`disabled` page action title, also used for infobox"
     },
     "pageActionUnknown": {
         "message": "The track is not recognized. Click to submit correct info.",
-        "description": "`unknown` page action title"
+        "description": "`unknown` page action title, also used for infobox"
     },
     "pageActionError": {
         "message": "A service error occurred. Click for more information.",
@@ -713,7 +713,7 @@
     },
     "pageActionUnsupported": {
         "message": "Web Scrobbler does not support this website.",
-        "description": "`unsupported` page action title"
+        "description": "`unsupported` page action title, also used for infobox"
     },
     "pageActionLoved": {
         "message": "You loved $1",
@@ -774,5 +774,17 @@
     "extDescription": {
         "message": "Scrobble music all around the web!",
         "description": "Description of the extension"
+    },
+    "infoBoxStateDisallowed": {
+        "message":  "Scrobbling is not allowed. Click web scrobbler extension icon for more information.",
+        "description": "Infobox text when an option is disallowing scrobbling"
+    },
+    "infoBoxStateError": {
+        "message": "A service error occurred. Click web scrobbler extension icon for more information.",
+        "description": "Infobox text when an error occured"
+    },
+    "infoBoxStateUnknown": {
+        "message": "The track is not recognized. Click web scrobbler extension icon to correct info.",
+        "description": "Infobox track is not recognized"
     }
 }
\ No newline at end of file
diff --git a/src/connectors/youtube.ts b/src/connectors/youtube.ts
index 7b5f25c3..e66a4ab5 100644
--- a/src/connectors/youtube.ts
+++ b/src/connectors/youtube.ts
@@ -86,6 +86,13 @@ setupEventListener();
 
 Connector.playerSelector = ['#content', '#player'];
 
+Connector.scrobbleInfoLocationSelector = '#primary #title.ytd-watch-metadata';
+Connector.scrobbleInfoStyle = {
+	...Connector.scrobbleInfoStyle,
+	fontSize: '1.17em',
+	fontWeight: '700',
+};
+
 Connector.getTrackInfo = () => {
 	const trackInfo: TrackInfoWithAlbum = {};
 
diff --git a/src/core/content/connector.ts b/src/core/content/connector.ts
index 33597468..c93ae896 100644
--- a/src/core/content/connector.ts
+++ b/src/core/content/connector.ts
@@ -129,6 +129,21 @@ export default class BaseConnector {
 	 */
 	public playerSelector: string | string[] | null = null;
 
+	/**
+	 * This selector is used to determine where to inject the infobox
+	 */
+	public scrobbleInfoLocationSelector: string | null = null;
+
+	/**
+	 * Styles to apply to the infobox
+	 * this is in camelCase so its fontSize, not font-size
+	 */
+	public scrobbleInfoStyle: Partial<CSSStyleDeclaration> = {
+		display: 'flex',
+		gap: '0.5em',
+		alignItems: 'center',
+	};
+
 	/**
 	 * Selector of element contains a track art of now playing song.
 	 * Default implementation looks for track art URL in `src` attribute or
diff --git a/src/core/content/reactor.ts b/src/core/content/reactor.ts
index 4f0f03db..30f1b11d 100644
--- a/src/core/content/reactor.ts
+++ b/src/core/content/reactor.ts
@@ -17,7 +17,7 @@ export default class Reactor {
 	 * @param isEnabled - Whether the connector is currently enabled
 	 */
 	constructor(connector: BaseConnector, isEnabled: boolean) {
-		this.controller = new Controller(connector.meta, isEnabled);
+		this.controller = new Controller(connector, isEnabled);
 
 		// Setup listening for state changes on connector.
 		connector.controllerCallback = this.onStateChanged.bind(this);
diff --git a/src/core/content/util.ts b/src/core/content/util.ts
index 8e1d5658..fc395aed 100644
--- a/src/core/content/util.ts
+++ b/src/core/content/util.ts
@@ -8,8 +8,13 @@ import type {
 	TrackInfoWithAlbum,
 } from '@/core/types';
 import type { ConnectorOptions } from '@/core/storage/options';
+import type { ControllerModeStr } from '@/core/object/controller/controller';
 import type { DebugLogType } from '@/util/util';
 
+import { t } from '@/util/i18n';
+import * as ControllerMode from '@/core/object/controller/controller-mode';
+import Song from '../object/song';
+
 const BrowserStorage = (async () => {
 	return import('@/core/storage/browser-storage');
 })();
@@ -867,13 +872,16 @@ export function processYtVideoTitle(
 	title = title.replace(/-\s*([「【『])/, '$1');
 
 	// 【/(*Music Video/MV/PV*】/)
-	title = title.replace(/[(【].*?((MV)|(PV)).*?[】)]/i, '');
+	title = title.replace(
+		/[(［【][^(［【]*?((Music Video)|(MV)|(PV)).*?[】］)]/i,
+		'',
+	);
 
 	// 【/(東方/オリジナル*】/)
-	title = title.replace(/[(【]((オリジナル)|(東方)).*?[】)]/, '');
+	title = title.replace(/[(［【]((オリジナル)|(東方)).*?[】］)]+?/, '');
 
 	// MV/PV if followed by an opening/closing bracket
-	title = title.replace(/(MV|PV)([「【『』】」])/i, '$2');
+	title = title.replace(/((?:Music Video)|MV|PV)([「［【『』】］」])/i, '$2');
 
 	// MV/PV if ending and with whitespace in front
 	title = title.replace(/\s+(MV|PV)$/i, '');
@@ -1035,3 +1043,25 @@ export function getOriginUrl(selector: string): string {
 		document.location.href
 	);
 }
+
+export function getInfoBoxText(
+	mode: ControllerModeStr | undefined,
+	song: Song | null,
+) {
+	if (!mode) {
+		return t('pageActionLoading');
+	}
+
+	const trackInfo = `${song?.getArtist()} - ${song?.getTrack()}`;
+	switch (mode) {
+		case ControllerMode.Disallowed:
+			return t('infoBoxStateDisallowed', trackInfo);
+		case ControllerMode.Err:
+			return t('infoBoxStateError');
+		case ControllerMode.Unknown:
+			return t('infoBoxStateUnknown');
+		default:
+			// re-use existing translation messages
+			return t(`pageAction${mode}`, trackInfo);
+	}
+}
diff --git a/src/core/object/controller/controller-mode.ts b/src/core/object/controller/controller-mode.ts
index 80d07466..aca926e8 100644
--- a/src/core/object/controller/controller-mode.ts
+++ b/src/core/object/controller/controller-mode.ts
@@ -4,7 +4,7 @@
 export const Base = 'Base';
 
 /**
- * An option is disallowing scrobbling.
+ * An option, or something hard coded into the connector, is disallowing scrobbling.
  */
 export const Disallowed = 'Disallowed';
 
diff --git a/src/core/object/controller/controller.ts b/src/core/object/controller/controller.ts
index 41b85102..8c470f8f 100644
--- a/src/core/object/controller/controller.ts
+++ b/src/core/object/controller/controller.ts
@@ -22,6 +22,8 @@ import {
 import EventEmitter from '@/util/emitter';
 import * as BrowserStorage from '@/core/storage/browser-storage';
 import { debugLog } from '@/core/content/util';
+import browser from 'webextension-polyfill';
+import BaseConnector from '@/core/content/connector';
 
 /**
  * List of song fields used to check if song is changed. If any of
@@ -58,7 +60,7 @@ const disabledTabs = BrowserStorage.getStorage(BrowserStorage.DISABLED_TABS);
  * Object that handles song playback and scrobbling actions.
  */
 export default class Controller {
-	public connector: ConnectorMeta;
+	public connector: BaseConnector;
 	public isEnabled: boolean;
 	public mode: ControllerModeStr;
 
@@ -89,17 +91,110 @@ export default class Controller {
 		payload: undefined,
 	});
 
+	/**
+	 * Function that handles updating the scrobble info box
+	 */
+	private async getInfoBoxElement(): Promise<HTMLDivElement | null> {
+		if (
+			!this.connector.scrobbleInfoLocationSelector ||
+			// infobox is disabled in options
+			!(await Options.getOption(
+				Options.USE_INFOBOX,
+				this.connector.meta.id,
+			))
+		) {
+			return null;
+		}
+
+		const parentEl = document.querySelector(
+			this.connector.scrobbleInfoLocationSelector,
+		);
+		if (!parentEl) {
+			return null;
+		}
+
+		// check if infoBoxEl was already created
+		let infoBoxElement = document.querySelector<HTMLDivElement>(
+			'#scrobbler-infobox-el',
+		);
+
+		// check if element is still in the correct place
+		if (infoBoxElement) {
+			if (infoBoxElement.parentElement !== parentEl) {
+				infoBoxElement.remove();
+			} else {
+				return infoBoxElement;
+			}
+		}
+
+		// if it was not in the correct place or didn't exist, create it
+		infoBoxElement = document.createElement('div');
+		infoBoxElement.setAttribute('id', 'scrobbler-infobox-el');
+
+		// style the infobox
+		for (const prop in this.connector.scrobbleInfoStyle) {
+			infoBoxElement.style[prop] =
+				this.connector.scrobbleInfoStyle[prop] ?? '';
+		}
+
+		parentEl.appendChild(infoBoxElement);
+		return infoBoxElement;
+	}
+
+	private async updateInfoBox() {
+		let oldInfoBoxText: string | false = false;
+		const infoBoxElement = await this.getInfoBoxElement();
+		if (!infoBoxElement) {
+			// clean up
+			const infoBoxElement = document.querySelector<HTMLDivElement>(
+				'#scrobbler-infobox-el',
+			);
+			if (infoBoxElement) {
+				infoBoxElement.remove();
+			}
+			return;
+		}
+		const textEl = infoBoxElement.querySelector('span');
+		if (textEl) {
+			oldInfoBoxText = textEl.innerText;
+		}
+
+		const mode = this.getMode();
+		const infoBoxText = Util.getInfoBoxText(mode, this.currentSong);
+
+		// Check if infobox needs to be updated
+		if (!oldInfoBoxText || infoBoxText !== oldInfoBoxText) {
+			const img = document.createElement('img');
+			img.setAttribute(
+				'src',
+				browser.runtime.getURL('./icons/icon_main_48.png'),
+			);
+			img.setAttribute('alt', 'Web Scrobbler state:');
+			img.setAttribute('style', 'height: 1.2em');
+
+			const info = document.createElement('span');
+			info.innerText = infoBoxText;
+
+			// Clear old contents of infoBoxElement
+			while (infoBoxElement.firstChild) {
+				infoBoxElement.removeChild(infoBoxElement.firstChild);
+			}
+			infoBoxElement.appendChild(img);
+			infoBoxElement.appendChild(info);
+		}
+	}
+
 	/**
 	 * @param tabId - Tab ID
 	 * @param connector - Connector match object
 	 * @param isEnabled - Flag indicates initial stage
 	 */
-	constructor(connector: ConnectorMeta, isEnabled: boolean) {
+	constructor(connector: BaseConnector, isEnabled: boolean) {
 		this.connector = connector;
 		this.isEnabled = isEnabled;
 		this.mode = isEnabled ? ControllerMode.Base : ControllerMode.Disabled;
 		this.setMode(this.mode);
-		Options.getOption(Options.SCROBBLE_PODCASTS, connector.id)
+		Options.getOption(Options.SCROBBLE_PODCASTS, connector.meta.id)
 			.then((shouldScrobblePodcasts) => {
 				if (typeof shouldScrobblePodcasts !== 'boolean') {
 					return;
@@ -110,7 +205,9 @@ export default class Controller {
 				debugLog(err, 'error');
 			});
 
-		this.debugLog(`Created controller for ${connector.label} connector`);
+		this.debugLog(
+			`Created controller for ${connector.meta.label} connector`,
+		);
 
 		setupContentListeners(
 			contentListener({
@@ -196,6 +293,7 @@ export default class Controller {
 	 * Called if current song is updated.
 	 */
 	public onSongUpdated(): void {
+		this.updateInfoBox();
 		sendContentMessage({
 			type: 'songUpdate',
 			payload: this.currentSong?.getCloneableData() ?? null,
@@ -206,6 +304,7 @@ export default class Controller {
 	 * Called if a controller mode is changed.
 	 */
 	public onModeChanged(): void {
+		this.updateInfoBox();
 		sendContentMessage({
 			type: 'controllerModeChange',
 			payload: this.mode,
@@ -222,18 +321,18 @@ export default class Controller {
 			case ControllerEvents.SongNowPlaying: {
 				const song = this.getCurrentSong();
 				if (!song || song.flags.isReplaying || !this.shouldScrobble()) {
-					return;
+					break;
 				}
 				const id = await this.tabId;
 				if (!id) {
-					return;
+					break;
 				}
 
 				sendContentMessage({
 					type: 'showNowPlaying',
 					payload: {
 						song: song.getCloneableData(),
-						connector: this.connector,
+						connector: this.connector.meta,
 					},
 				});
 				break;
@@ -253,24 +352,25 @@ export default class Controller {
 			case ControllerEvents.SongUnrecognized: {
 				const song = this.getCurrentSong();
 				if (!song || !this.shouldScrobble()) {
-					return;
+					break;
 				}
 				const id = await this.tabId;
 				if (!id) {
-					return;
+					break;
 				}
 
 				sendContentMessage({
 					type: 'showSongNotRecognized',
 					payload: {
 						song: song.getCloneableData(),
-						connector: this.connector,
+						connector: this.connector.meta,
 					},
 				});
 				break;
 			}
 		}
-		// do nothing
+
+		this.updateInfoBox();
 	}
 
 	/** Public functions */
@@ -295,7 +395,7 @@ export default class Controller {
 	 */
 	public finish(): void {
 		this.debugLog(
-			`Remove controller for ${this.connector.label} connector`,
+			`Remove controller for ${this.connector.meta.label} connector`,
 		);
 		this.resetState();
 	}
@@ -337,7 +437,7 @@ export default class Controller {
 	 * @returns Connector
 	 */
 	getConnector(): ConnectorMeta {
-		return this.connector;
+		return this.connector.meta;
 	}
 
 	/**
@@ -501,7 +601,7 @@ export default class Controller {
 		 * clear any previous song and its bindings.
 		 */
 		this.resetState();
-		this.currentSong = new Song(newState, this.connector);
+		this.currentSong = new Song(newState, this.connector.meta);
 		this.currentSong.flags.isReplaying = this.isReplayingSong;
 
 		this.debugLog(
@@ -606,7 +706,12 @@ export default class Controller {
 			this.setMode(ControllerMode.Disallowed);
 		}
 
-		if (!(await this.pipeline.process(this.currentSong, this.connector))) {
+		if (
+			!(await this.pipeline.process(
+				this.currentSong,
+				this.connector.meta,
+			))
+		) {
 			return;
 		}
 
@@ -765,7 +870,7 @@ export default class Controller {
 
 		const percent = await Options.getOption(
 			Options.SCROBBLE_PERCENT,
-			this.connector.id,
+			this.connector.meta.id,
 		);
 		if (typeof percent !== 'number') {
 			return;
@@ -869,7 +974,7 @@ export default class Controller {
 			...disabledTabList,
 			[currentTab ?? -1]: {
 				...(disabledTabList?.[currentTab ?? -1] ?? {}),
-				[this.connector.id]: true,
+				[this.connector.meta.id]: true,
 			},
 		});
 		this.setEnabled(false);
diff --git a/src/core/scrobbler/webhook-scrobbler.ts b/src/core/scrobbler/webhook-scrobbler.ts
index 921762dc..f27141d4 100644
--- a/src/core/scrobbler/webhook-scrobbler.ts
+++ b/src/core/scrobbler/webhook-scrobbler.ts
@@ -9,6 +9,7 @@ import ClonedSong from '../object/cloned-song';
 
 type WebhookRequest = {
 	eventName: string;
+	time: number;
 	data: {
 		song: BaseSong;
 		isLoved?: boolean;
@@ -120,6 +121,7 @@ export default class WebhookScrobbler extends BaseScrobbler<'Webhook'> {
 	async sendNowPlaying(song: BaseSong): Promise<ServiceCallResult> {
 		return this.sendRequest({
 			eventName: 'nowplaying',
+			time: Date.now(),
 			data: { song },
 		});
 	}
@@ -128,6 +130,7 @@ export default class WebhookScrobbler extends BaseScrobbler<'Webhook'> {
 	async sendPaused(song: BaseSong): Promise<ServiceCallResult> {
 		return this.sendRequest({
 			eventName: 'paused',
+			time: Date.now(),
 			data: { song },
 		});
 	}
@@ -136,6 +139,7 @@ export default class WebhookScrobbler extends BaseScrobbler<'Webhook'> {
 	async sendResumedPlaying(song: BaseSong): Promise<ServiceCallResult> {
 		return this.sendRequest({
 			eventName: 'resumedplaying',
+			time: Date.now(),
 			data: { song },
 		});
 	}
@@ -144,6 +148,7 @@ export default class WebhookScrobbler extends BaseScrobbler<'Webhook'> {
 	public async scrobble(song: BaseSong): Promise<ServiceCallResult> {
 		return this.sendRequest({
 			eventName: 'scrobble',
+			time: Date.now(),
 			data: { song },
 		});
 	}
@@ -155,6 +160,7 @@ export default class WebhookScrobbler extends BaseScrobbler<'Webhook'> {
 	): Promise<ServiceCallResult | Record<string, never>> {
 		return this.sendRequest({
 			eventName: 'loved',
+			time: Date.now(),
 			data: { song, isLoved },
 		});
 	}
diff --git a/src/core/storage/options.ts b/src/core/storage/options.ts
index 9be3fe8c..fe5c318e 100644
--- a/src/core/storage/options.ts
+++ b/src/core/storage/options.ts
@@ -13,6 +13,7 @@ const connectorsOverrideOptions = BrowserStorage.getStorage(
 export const USE_NOTIFICATIONS = 'useNotifications';
 export const USE_UNRECOGNIZED_SONG_NOTIFICATIONS =
 	'useUnrecognizedSongNotifications';
+export const USE_INFOBOX = 'showInfobox';
 export const SCROBBLE_PODCASTS = 'scrobblePodcasts';
 export const FORCE_RECOGNIZE = 'forceRecognize';
 export const SCROBBLE_RECOGNIZED_TRACKS = 'scrobbleRecognizedTracks';
@@ -32,6 +33,11 @@ export interface GlobalOptions {
 	 */
 	[USE_NOTIFICATIONS]: boolean;
 
+	/**
+	 * Show the infobox for supported connectors
+	 */
+	[USE_INFOBOX]: boolean;
+
 	/**
 	 * Scrobble percent.
 	 */
@@ -82,6 +88,7 @@ const DEFAULT_OPTIONS: GlobalOptions = {
 	[SCROBBLE_EDITED_TRACKS_ONLY]: false,
 	[DEBUG_LOGGING_ENABLED]: false,
 	[SCROBBLE_PERCENT]: 50,
+	[USE_INFOBOX]: true,
 	[DISABLED_CONNECTORS]: {},
 };
 
@@ -92,6 +99,7 @@ const OVERRIDE_CONTENT = {
 	[SCROBBLE_PODCASTS]: true,
 	[USE_NOTIFICATIONS]: true,
 	[USE_UNRECOGNIZED_SONG_NOTIFICATIONS]: false,
+	[USE_INFOBOX]: true,
 };
 
 export interface ConnectorOptions {
@@ -118,6 +126,7 @@ const DEFAULT_CONNECTOR_OPTIONS: ConnectorOptions = {
 export interface ConnectorsOverrideOptionValues {
 	[FORCE_RECOGNIZE]?: boolean;
 	[USE_NOTIFICATIONS]?: boolean;
+	[USE_INFOBOX]?: boolean;
 	[SCROBBLE_PODCASTS]?: boolean;
 	[USE_UNRECOGNIZED_SONG_NOTIFICATIONS]?: boolean;
 	[SCROBBLE_RECOGNIZED_TRACKS]?: boolean;
diff --git a/src/ui/options/components/accounts.tsx b/src/ui/options/components/accounts.tsx
index c86044d7..64000ad1 100644
--- a/src/ui/options/components/accounts.tsx
+++ b/src/ui/options/components/accounts.tsx
@@ -3,11 +3,15 @@ import ScrobbleService, {
 	Scrobbler,
 	ScrobblerLabel,
 } from '@/core/object/scrobble-service';
-import { For, Show, createResource, onCleanup } from 'solid-js';
+import { For, Show, createResource, createSignal, onCleanup } from 'solid-js';
 import styles from './components.module.scss';
 import browser from 'webextension-polyfill';
 import Delete from '@suid/icons-material/DeleteOutlined';
+import Save from '@suid/icons-material/SaveOutlined';
+import AutoRenew from '@suid/icons-material/AutorenewOutlined';
+import Check from '@suid/icons-material/CheckOutlined';
 import { debugLog } from '@/util/util';
+import { Dynamic } from 'solid-js/web';
 
 /**
  * Properties associated with each scrobbler, and the input type to use for the user to edit them.
@@ -178,6 +182,45 @@ function SignedOut(props: { scrobbler: Scrobbler }) {
 	);
 }
 
+enum SaveState {
+	BASE = 'base',
+	SAVING = 'saving',
+	SAVED = 'saved',
+}
+
+/**
+ * Save button component.
+ * This button doesn't actually do anything as the extension autosaves, but better user feedback :)
+ */
+function SaveButton() {
+	const [state, setState] = createSignal(SaveState.BASE);
+
+	const icons = {
+		[SaveState.BASE]: Save,
+		[SaveState.SAVING]: AutoRenew,
+		[SaveState.SAVED]: Check,
+	};
+
+	return (
+		<button
+			class={styles.resetButton}
+			onClick={() => {
+				if (state() !== SaveState.SAVING) {
+					setState(SaveState.SAVING);
+					setTimeout(() => {
+						setState(SaveState.SAVED);
+					}, 850);
+				}
+			}}
+		>
+			<div class={`${styles[`${state()}SaveIcon`]} ${styles.saveIcon}`}>
+				<Dynamic component={icons[state()]} />
+			</div>
+			Save
+		</button>
+	);
+}
+
 /**
  * Component that allows the user to edit scrobbler properties for the scrobblers that support them.
  */
@@ -221,6 +264,7 @@ function Properties(props: { scrobbler: Scrobbler }) {
 					);
 				}}
 			</For>
+			<SaveButton />
 		</>
 	);
 }
diff --git a/src/ui/options/components/components.module.scss b/src/ui/options/components/components.module.scss
index 9acfb542..6a107b67 100644
--- a/src/ui/options/components/components.module.scss
+++ b/src/ui/options/components/components.module.scss
@@ -493,3 +493,13 @@ summary {
 		grid-area: albumartistreplace;
 	}
 }
+
+.saveIcon {
+	transform: rotate(0deg);
+
+	&.savingSaveIcon {
+		transform: rotate(360deg);
+		transform-origin: 41% 42.5%;
+		transition: transform 600ms ease-in-out;
+	}
+}
diff --git a/src/ui/options/components/connector-override.tsx b/src/ui/options/components/connector-override.tsx
index b078fd16..54a7c0fc 100644
--- a/src/ui/options/components/connector-override.tsx
+++ b/src/ui/options/components/connector-override.tsx
@@ -205,6 +205,15 @@ function ConnectorOverrideOptionDetails(props: {
 					connectorOverrideOptions={connectorOverrideOptions}
 				/>
 			</Show>
+			<ConnectorTripleCheckbox
+				title={t('optionUseInfoboxTitle')}
+				label={t('optionUseInfobox')}
+				connector={connector}
+				option={Options.USE_INFOBOX}
+				overrideOptions={overrideOptions}
+				setOverrideOptions={setOverrideOptions}
+				connectorOverrideOptions={connectorOverrideOptions}
+			/>
 			<ConnectorTripleCheckbox
 				title={t('optionScrobblePodcastsTitle')}
 				label={t('optionScrobblePodcasts')}
diff --git a/src/ui/options/components/options/global-options.tsx b/src/ui/options/components/options/global-options.tsx
index 4fd0cb60..0f1b99c0 100644
--- a/src/ui/options/components/options/global-options.tsx
+++ b/src/ui/options/components/options/global-options.tsx
@@ -49,6 +49,14 @@ export default function GlobalOptionsList(props: {
 						key={Options.USE_UNRECOGNIZED_SONG_NOTIFICATIONS}
 					/>
 				</Show>
+				<GlobalOptionEntry
+					options={props.options}
+					setOptions={props.setOptions}
+					globalOptions={globalOptions}
+					i18ntitle="optionUseInfoboxTitle"
+					i18nlabel="optionUseInfobox"
+					key={Options.USE_INFOBOX}
+				/>
 				<GlobalOptionEntry
 					options={props.options}
 					setOptions={props.setOptions}


Test patch:
diff --git a/tests/content/util.test.ts b/tests/content/util.test.ts
index 0a2f76c6..75b548f0 100644
--- a/tests/content/util.test.ts
+++ b/tests/content/util.test.ts
@@ -1,12 +1,8 @@
 import { TestData } from '#/types/types';
 import { expect, it, describe } from 'vitest';
 
-// trick webextension polyfill into thinking this is running in the browser
-(globalThis as unknown as { chrome: unknown }).chrome = {
-	runtime: {
-		id: 'mock',
-	},
-};
+import webextensionPolyfill from '#/mocks/webextension-polyfill';
+webextensionPolyfill.reset();
 import * as Util from '@/core/content/util';
 
 /**
@@ -670,6 +666,11 @@ const PROCESS_YT_VIDEO_TITLE_DATA = [
 		args: ['Artist「Track」【stuff】'],
 		expected: { artist: 'Artist', track: 'Track' },
 	},
+	{
+		description: 'should remove ［Music Video］ string',
+		args: ['Artist - Track(feat.Artist2)［Music Video］'],
+		expected: { artist: 'Artist', track: 'Track(feat.Artist2)' },
+	},
 ];
 
 /**