From: Marcel van der Veldt Date: Mon, 15 Dec 2025 23:44:08 +0000 (+0100) Subject: Add user filter to scrobble providers (#2822) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=862d845fa36b3f966f219274ad84abcdc5c7c04d;p=music-assistant-server.git Add user filter to scrobble providers (#2822) --- diff --git a/music_assistant/helpers/scrobbler.py b/music_assistant/helpers/scrobbler.py index 81af9a34..47b3aaf6 100644 --- a/music_assistant/helpers/scrobbler.py +++ b/music_assistant/helpers/scrobbler.py @@ -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=[], + ) diff --git a/music_assistant/providers/lastfm_scrobble/__init__.py b/music_assistant/providers/lastfm_scrobble/__init__.py index 306e0853..8a557375 100644 --- a/music_assistant/providers/lastfm_scrobble/__init__.py +++ b/music_assistant/providers/lastfm_scrobble/__init__.py @@ -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 diff --git a/music_assistant/providers/listenbrainz_scrobble/__init__.py b/music_assistant/providers/listenbrainz_scrobble/__init__.py index 33dfe99f..7541bb24 100644 --- a/music_assistant/providers/listenbrainz_scrobble/__init__.py +++ b/music_assistant/providers/listenbrainz_scrobble/__init__.py @@ -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), )