Add user filter to scrobble providers (#2822)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 15 Dec 2025 23:44:08 +0000 (00:44 +0100)
committerGitHub <noreply@github.com>
Mon, 15 Dec 2025 23:44:08 +0000 (00:44 +0100)
music_assistant/helpers/scrobbler.py
music_assistant/providers/lastfm_scrobble/__init__.py
music_assistant/providers/listenbrainz_scrobble/__init__.py

index 81af9a34276dde7482c3d6fbef4875657921d6eb..47b3aaf608d08699f104404d477a3e84b4037b8b 100644 (file)
@@ -3,11 +3,12 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, cast
 
 from music_assistant_models.config_entries import (
     Config,
     ConfigEntry,
+    ConfigValueOption,
     ConfigValueType,
 )
 from music_assistant_models.enums import ConfigEntryType
@@ -16,6 +17,8 @@ if TYPE_CHECKING:
     from music_assistant_models.event import MassEvent
     from music_assistant_models.playback_progress_report import MediaItemPlaybackProgressReport
 
+    from music_assistant import MusicAssistant
+
 
 class ScrobblerHelper:
     """Base class to aid scrobbling tracks."""
@@ -54,6 +57,11 @@ class ScrobblerHelper:
 
         report: MediaItemPlaybackProgressReport = event.data
 
+        # handle optional user_id filtering
+        if self.config.mass_userids and report.userid not in self.config.mass_userids:
+            self.logger.debug("skipped scrobbling for user %s due to user filter", report.userid)
+            return
+
         # poor mans attempt to detect a song on loop
         if not report.fully_played and report.uri == self.last_scrobbled:
             self.logger.debug(
@@ -104,14 +112,16 @@ class ScrobblerHelper:
 
 
 CONF_VERSION_SUFFIX = "suffix_version"
+CONF_SCROBBLE_USERS = "scrobble_users"
 
 
 class ScrobblerConfig:
     """Shared configuration options for scrobblers."""
 
-    def __init__(self, suffix_version: bool) -> None:
+    def __init__(self, suffix_version: bool, mass_userids: list[str] | None = None) -> None:
         """Initialize."""
         self.suffix_version = suffix_version
+        self.mass_userids = mass_userids or []
 
     @staticmethod
     def get_shared_config_entries(values: dict[str, ConfigValueType] | None) -> list[ConfigEntry]:
@@ -132,4 +142,29 @@ class ScrobblerConfig:
     @staticmethod
     def create_from_config(config: Config) -> ScrobblerConfig:
         """Extract relevant shared config values."""
-        return ScrobblerConfig(bool(config.get_value(CONF_VERSION_SUFFIX, True)))
+        return ScrobblerConfig(
+            suffix_version=bool(config.get_value(CONF_VERSION_SUFFIX, True)),
+            mass_userids=cast("list[str]", config.get_value(CONF_SCROBBLE_USERS, [])),
+        )
+
+
+async def create_scrobble_users_config_entry(mass: MusicAssistant) -> ConfigEntry:
+    """Create a reusable configentry to specify a userlist for scrobbling providers."""
+    # User options for scrobble filtering
+    ma_user_list = await mass.webserver.auth.list_users()  # excludes system users
+    ma_user_list = [user for user in ma_user_list if user.enabled]
+    user_options = [
+        ConfigValueOption(title=user.display_name or user.username, value=user.user_id)
+        for user in ma_user_list
+    ]
+    return ConfigEntry(
+        key=CONF_SCROBBLE_USERS,
+        type=ConfigEntryType.STRING,
+        label="Scrobble for users",
+        required=False,
+        description="Only register scrobbles for the selected users. "
+        "Leave empty to scrobble for all users.",
+        options=user_options,
+        multi_value=True,
+        default_value=[],
+    )
index 306e08530fc5c9112bfc1e51fa8b44210c467a7d..8a5573754d2a5811adbd82fea61f07a0f8f66b1a 100644 (file)
@@ -21,7 +21,11 @@ from music_assistant_models.provider import ProviderManifest
 
 from music_assistant.constants import MASS_LOGGER_NAME
 from music_assistant.helpers.auth import AuthenticationHelper
-from music_assistant.helpers.scrobbler import ScrobblerConfig, ScrobblerHelper
+from music_assistant.helpers.scrobbler import (
+    ScrobblerConfig,
+    ScrobblerHelper,
+    create_scrobble_users_config_entry,
+)
 from music_assistant.mass import MusicAssistant
 from music_assistant.models import ProviderInstanceType
 from music_assistant.models.plugin import PluginProvider
@@ -199,6 +203,8 @@ async def get_config_entries(
             required=True,
             value=values.get(CONF_API_SECRET) if values else None,
         ),
+        # add user selection entry
+        await create_scrobble_users_config_entry(mass),
     ]
 
     # early return so we can assume values are present
index 33dfe99fb18845c28f78585d2b17fdbe070726b2..7541bb24f9cf2c21688d728c27cbc9375849a394 100644 (file)
@@ -17,7 +17,11 @@ from music_assistant_models.errors import SetupFailedError
 from music_assistant_models.playback_progress_report import MediaItemPlaybackProgressReport
 from music_assistant_models.provider import ProviderManifest
 
-from music_assistant.helpers.scrobbler import ScrobblerConfig, ScrobblerHelper
+from music_assistant.helpers.scrobbler import (
+    ScrobblerConfig,
+    ScrobblerHelper,
+    create_scrobble_users_config_entry,
+)
 from music_assistant.mass import MusicAssistant
 from music_assistant.models import ProviderInstanceType
 from music_assistant.models.plugin import PluginProvider
@@ -135,7 +139,7 @@ class ListenBrainzEventHandler(ScrobblerHelper):
 
 
 async def get_config_entries(
-    mass: MusicAssistant,  # noqa: ARG001
+    mass: MusicAssistant,
     instance_id: str | None = None,  # noqa: ARG001
     action: str | None = None,  # noqa: ARG001
     values: dict[str, ConfigValueType] | None = None,
@@ -150,4 +154,6 @@ async def get_config_entries(
             required=True,
             value=values.get(CONF_USER_TOKEN) if values else None,
         ),
+        # add user selection entry
+        await create_scrobble_users_config_entry(mass),
     )