155 files

This commit is contained in:
Home Assistant Version Control committed 2026-10-04 20:46:32 +00:00
1 parent a212615470
commit 0ff8c287a8
155 files changed
+19264 -3148

No files matched your search

+134 -41
View File
@@ -14,17 +14,23 @@ from homeassistant.components.websocket_api import (
async_register_command,
websocket_command,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import issue_registry as ir
from homeassistant.loader import async_get_integration
from .api import ImageProxyView, MAThumbnailView, MAQueueView, MusicAssistantBrowseView, MusicAssistantLibraryView, MusicAssistantProvidersView, MusicAssistantRecommendationsView, MusicAssistantSearchView, MusicAssistantSubitemsView, PlayerGroupView, PlayerQueueJumpView, PlayerQueueView
from .const import CARD_JS_FILENAME, CARD_URL, CONF_DEBUG_MODE, CONF_EXCLUDED_PLAYERS, CONF_MA_URL, DOMAIN, ICON_URL, MUSIC_ASSISTANT_DOMAIN, WS_CONFIG_COMMAND
from .api import ImageProxyView, MAThumbnailView, MAQueueView, MusicAssistantBrowseView, MusicAssistantLibraryView, MusicAssistantProvidersView, MusicAssistantRecommendationsView, MusicAssistantSearchView, MusicAssistantSubitemsView, OutputsView, PlayerQueueJumpView, PlayerQueueView
from .const import CARD_JS_FILENAME, CARD_URL, CONF_DEBUG_MODE, CONF_DEFAULT_PLAYER, CONF_DEFAULT_TAB, CONF_EXCLUDED_PLAYERS, CONF_MA_URL, CONFIG_ENTRY_VERSION, DEFAULT_TAB, DOMAIN, ICON_URL, WS_CONFIG_COMMAND, WS_SUBSCRIBE_QUEUE_COMMAND
from .duplicates import ISSUE_DUPLICATE_ENTITIES, async_track_duplicates, async_update_issue
from .mass_connection import MyMusicLibraryConfigEntry, async_connect, async_disconnect
from .queue_push import async_relay_queue_events, ws_subscribe_queue
from .queue_watchdog import QueueWatchdog
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[str] = []
PLATFORMS: list[str] = ["media_player"]
WWW_DIR = os.path.join(os.path.dirname(__file__), "www")
ICON_PATH = os.path.join(os.path.dirname(__file__), "brand", "icon.png")
@@ -34,11 +40,29 @@ _QUEUE_STORE_VERSION = 1
_INTEGRATION_LOGGER = logging.getLogger("custom_components.my_music_library")
# Level the integration logger had before debug mode forced it to DEBUG
# (None while debug mode is off). Restored when debug mode is turned off, so
# the user's `logger:` configuration in configuration.yaml applies again.
_level_before_debug: int | None = None
def _apply_debug_mode(debug: bool) -> None:
"""Set the integration logger level based on the debug_mode option."""
_INTEGRATION_LOGGER.setLevel(logging.DEBUG if debug else logging.WARNING)
_INTEGRATION_LOGGER.debug("Debug mode %s", "enabled" if debug else "disabled")
"""Force DEBUG while the debug_mode option is on; otherwise leave the level alone.
Debug off never sets a level of its own: whatever `logger:` (or the
`logger.set_level` action) configured for custom_components.my_music_library
stays in effect.
"""
global _level_before_debug
if debug:
if _level_before_debug is None:
_level_before_debug = _INTEGRATION_LOGGER.level
_INTEGRATION_LOGGER.setLevel(logging.DEBUG)
_INTEGRATION_LOGGER.debug("Debug mode enabled")
elif _level_before_debug is not None:
_INTEGRATION_LOGGER.debug("Debug mode disabled")
_INTEGRATION_LOGGER.setLevel(_level_before_debug)
_level_before_debug = None
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
@@ -49,12 +73,31 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
stored = await store.async_load() or {}
hass.data[DOMAIN]["queue_store"] = store
hass.data[DOMAIN]["queues"] = stored.get("queues", {})
hass.data[DOMAIN]["groups"] = stored.get("groups", {})
# "groups" (card-side copy of group members, < 4.8.0) is dropped: MA is the source of truth.
hass.data[DOMAIN]["presets"] = stored.get("presets", [])
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up My Music Library from a config entry."""
async def async_save_store(hass: HomeAssistant) -> None:
"""Persist the per-player queues and the group presets."""
domain_data = hass.data.get(DOMAIN, {})
if store := domain_data.get("queue_store"):
await store.async_save({
"queues": domain_data.get("queues", {}),
"presets": domain_data.get("presets", []),
})
async def async_setup_entry(hass: HomeAssistant, entry: MyMusicLibraryConfigEntry) -> bool:
"""Set up My Music Library from a config entry.
Card serving, HTTP views and the WS config command are registered FIRST and
are independent of the Music Assistant connection succeeding: the card must
always be able to load and ask "am I connected?" — including right after a
failed/expired-token setup — so it can show a clear "needs reconfiguration"
message instead of just going blank. Only the media_player platform (which
needs a live client) depends on the connection below.
"""
hass.data.setdefault(DOMAIN, {})
# Serve the card JS file from /my_music_library/<filename>
@@ -63,7 +106,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error("Card JS file not found: %s", card_js_path)
raise ConfigEntryNotReady(f"Missing frontend file: {card_js_path}")
# Guard against double-registration (HA may call setup_entry on reload/restart)
# Guard against double-registration (HA may call setup_entry on reload —
# including our own auto-reload-on-disconnect in mass_connection.py)
registered_paths: set[str] = hass.data[DOMAIN].setdefault("_registered_paths", set())
static_registrations: list[StaticPathConfig] = []
@@ -98,46 +142,97 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await _async_register_lovelace_resource(hass, versioned_card_url, CARD_URL)
# Register HTTP proxy views (search + library + subitems → MA server)
hass.http.register_view(MusicAssistantSearchView)
hass.http.register_view(MusicAssistantLibraryView)
hass.http.register_view(MusicAssistantSubitemsView)
hass.http.register_view(PlayerQueueView)
hass.http.register_view(PlayerQueueJumpView)
hass.http.register_view(MAQueueView)
hass.http.register_view(PlayerGroupView)
hass.http.register_view(MusicAssistantBrowseView)
hass.http.register_view(MusicAssistantRecommendationsView)
hass.http.register_view(MusicAssistantProvidersView)
hass.http.register_view(ImageProxyView)
hass.http.register_view(MAThumbnailView)
# Register WebSocket command so the card can fetch its config
_register_websocket_commands(hass)
hass.data[DOMAIN][entry.entry_id] = {"entry": entry}
# Register HTTP proxy views + WS config command once per HA process lifetime
# (guarded the same way as the static paths above, for the same reload reason).
if not hass.data[DOMAIN].get("_views_registered"):
hass.http.register_view(MusicAssistantSearchView)
hass.http.register_view(MusicAssistantLibraryView)
hass.http.register_view(MusicAssistantSubitemsView)
hass.http.register_view(PlayerQueueView)
hass.http.register_view(PlayerQueueJumpView)
hass.http.register_view(MAQueueView)
hass.http.register_view(OutputsView)
hass.http.register_view(MusicAssistantBrowseView)
hass.http.register_view(MusicAssistantRecommendationsView)
hass.http.register_view(MusicAssistantProvidersView)
hass.http.register_view(ImageProxyView)
hass.http.register_view(MAThumbnailView)
_register_websocket_commands(hass)
hass.data[DOMAIN]["_views_registered"] = True
_apply_debug_mode(entry.options.get(CONF_DEBUG_MODE, False))
entry.async_on_unload(entry.add_update_listener(_async_options_updated))
# From here on, failure raises ConfigEntryNotReady/ConfigEntryAuthFailed —
# everything registered above stays in place so the card can still load and
# report the "not connected" state via the WS config command.
entry.runtime_data = await async_connect(hass, entry)
async def _on_hass_stop(event: Event) -> None:
await async_disconnect(entry.runtime_data)
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_hass_stop)
)
# Push queue changes to the card (my_music_library/subscribe_queue).
entry.async_on_unload(async_relay_queue_events(hass, entry.runtime_data.mass))
# Resume queues Music Assistant leaves stopped after the first track (queue_watchdog.py).
entry.async_on_unload(QueueWatchdog(hass, entry.runtime_data.mass).async_start())
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Duplicates with the official Music Assistant integration's media_players:
# check now (the entity registry is already loaded from disk), then again on
# every media_player registry change.
async_update_issue(hass)
entry.async_on_unload(async_track_duplicates(hass))
return True
async def async_migrate_entry(hass: HomeAssistant, entry: MyMusicLibraryConfigEntry) -> bool:
"""Migrate an old config entry to the current schema.
Version 1 covers both 3.x entries (MA connection borrowed from `mass`,
default player chosen in the setup step, so stored in `data`) and early
4.x ones (URL + token in `data`, default player already in `options`).
Version 2 keeps the default player in `options` only. A 3.x entry has no
token yet: setup then raises ConfigEntryAuthFailed, which sends the user
through the reauth step to enter one.
"""
if entry.version > CONFIG_ENTRY_VERSION:
# Downgrade from a newer, unknown schema.
return False
if entry.version == 1:
data = dict(entry.data)
options = dict(entry.options)
legacy_player = data.pop(CONF_DEFAULT_PLAYER, None)
if legacy_player and not options.get(CONF_DEFAULT_PLAYER):
options[CONF_DEFAULT_PLAYER] = legacy_player
hass.config_entries.async_update_entry(entry, data=data, options=options, version=2)
_LOGGER.info("Migrated config entry %s to version 2", entry.entry_id)
return True
async def _async_options_updated(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: MyMusicLibraryConfigEntry
) -> None:
"""React to options changes (debug toggle, excluded players, etc.)."""
_apply_debug_mode(entry.options.get(CONF_DEBUG_MODE, False))
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: MyMusicLibraryConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id, None)
await async_disconnect(entry.runtime_data)
ir.async_delete_issue(hass, DOMAIN, ISSUE_DUPLICATE_ENTITIES)
# Don't leave the logger stuck at DEBUG once the integration is gone.
_apply_debug_mode(False)
return unload_ok
@@ -152,28 +247,26 @@ def _register_websocket_commands(hass: HomeAssistant) -> None:
"""Return the integration config to the card."""
entries = hass.config_entries.async_entries(DOMAIN)
if not entries:
connection.send_result(msg["id"], {"ma_url": None})
connection.send_result(msg["id"], {"ma_url": None, "connected": False})
return
entry = entries[0]
# Find the Music Assistant config entry so the card can call MA's WS commands
ma_entries = hass.config_entries.async_entries(MUSIC_ASSISTANT_DOMAIN)
ma_entry_id = ma_entries[0].entry_id if ma_entries else None
connection.send_result(
msg["id"],
{
"connected": entry.state == ConfigEntryState.LOADED,
"ma_url": entry.data.get(CONF_MA_URL) or None,
"ma_entry_id": ma_entry_id,
"default_player": entry.data.get("default_player") or None,
"default_tab": entry.data.get("default_tab", "player"),
"default_player": entry.options.get(CONF_DEFAULT_PLAYER) or None,
"default_tab": entry.data.get(CONF_DEFAULT_TAB, DEFAULT_TAB),
"excluded_players": list(entry.options.get(CONF_EXCLUDED_PLAYERS, [])),
"debug_mode": bool(entry.options.get(CONF_DEBUG_MODE, False)),
},
)
async_register_command(hass, ws_get_config)
_LOGGER.debug("Registered WebSocket command: %s", WS_CONFIG_COMMAND)
async_register_command(hass, ws_subscribe_queue)
_LOGGER.debug("Registered WebSocket commands: %s, %s", WS_CONFIG_COMMAND, WS_SUBSCRIBE_QUEUE_COMMAND)
async def _async_register_lovelace_resource(
File diff suppressed because it is too large. Load diff
+120 -43
View File
@@ -5,14 +5,21 @@ from typing import Any
from urllib.parse import urlparse
import voluptuous as vol
from music_assistant_client import MusicAssistantClient
from music_assistant_client.exceptions import CannotConnect, InvalidServerVersion
from music_assistant_models.errors import AuthenticationFailed, AuthenticationRequired, MusicAssistantError
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .const import (
@@ -20,49 +27,38 @@ from .const import (
CONF_DEFAULT_PLAYER,
CONF_DEFAULT_TAB,
CONF_EXCLUDED_PLAYERS,
CONF_MA_TOKEN,
CONF_MA_URL,
DEFAULT_MA_URL,
CONFIG_ENTRY_VERSION,
DEFAULT_TAB,
DOMAIN,
MUSIC_ASSISTANT_DOMAIN,
NAME,
)
def _get_ma_players(hass: HomeAssistant) -> dict[str, str]:
"""Return a dict of {entity_id: friendly_name} for Music Assistant media_players."""
def _get_mml_players(hass: HomeAssistant) -> dict[str, str]:
"""Return {entity_id: friendly_name} for our own media_player entities."""
ent_reg = er.async_get(hass)
players: dict[str, str] = {}
for entity in ent_reg.entities.values():
if (
entity.platform == MUSIC_ASSISTANT_DOMAIN
and entity.domain == "media_player"
and not entity.disabled
):
if entity.platform == DOMAIN and entity.domain == "media_player" and not entity.disabled:
state = hass.states.get(entity.entity_id)
name = (
state.attributes.get("friendly_name", entity.entity_id)
if state
else entity.entity_id
)
name = state.attributes.get("friendly_name", entity.entity_id) if state else entity.entity_id
players[entity.entity_id] = name
return players
def _get_all_players(hass: HomeAssistant) -> dict[str, str]:
"""Return a dict of {entity_id: friendly_name} for all non-unavailable media_player entities."""
"""Return {entity_id: friendly_name} for all non-unavailable media_player entities."""
players: dict[str, str] = {}
for state in hass.states.async_all("media_player"):
if state.state != "unavailable":
name = state.attributes.get("friendly_name", state.entity_id)
players[state.entity_id] = name
players[state.entity_id] = state.attributes.get("friendly_name", state.entity_id)
return dict(sorted(players.items(), key=lambda x: x[1].lower()))
def _validate_url(url: str) -> str | None:
"""Return None if valid, or an error key if invalid."""
if not url:
return None # empty = not configured, that's fine
try:
parsed = urlparse(url)
if parsed.scheme not in ("http", "https") or not parsed.netloc:
@@ -72,10 +68,29 @@ def _validate_url(url: str) -> str | None:
return None
async def _test_connection(hass: HomeAssistant, url: str, token: str) -> str | None:
"""Try connecting to the MA server. Return None on success, else an error key."""
session = async_get_clientsession(hass)
mass = MusicAssistantClient(url, session, token=token)
try:
await mass.connect()
except (AuthenticationRequired, AuthenticationFailed):
return "invalid_auth"
except InvalidServerVersion:
return "invalid_server_version"
except (CannotConnect, TimeoutError):
return "cannot_connect"
except MusicAssistantError:
return "cannot_connect"
else:
await mass.disconnect()
return None
class MyMusicLibraryConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for My Music Library."""
VERSION = 1
VERSION = CONFIG_ENTRY_VERSION
@staticmethod
@callback
@@ -86,29 +101,37 @@ class MyMusicLibraryConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
"""Handle the initial step: Music Assistant server URL + long-lived token."""
await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured()
errors: dict[str, str] = {}
ma_players = _get_ma_players(self.hass)
if user_input is not None:
url_error = _validate_url(user_input.get(CONF_MA_URL, ""))
if url_error:
url = user_input[CONF_MA_URL].rstrip("/")
token = user_input[CONF_MA_TOKEN]
if url_error := _validate_url(url):
errors[CONF_MA_URL] = url_error
else:
return self.async_create_entry(title=NAME, data=user_input)
player_options = {
"": "Auto-detect (first available player)",
**ma_players,
}
error = await _test_connection(self.hass, url, token)
if error:
errors["base"] = error
else:
return self.async_create_entry(
title=NAME,
data={
CONF_MA_URL: url,
CONF_MA_TOKEN: token,
CONF_DEFAULT_TAB: user_input[CONF_DEFAULT_TAB],
},
)
schema = vol.Schema(
{
vol.Optional(CONF_MA_URL, default=DEFAULT_MA_URL): str,
vol.Optional(CONF_DEFAULT_PLAYER, default=""): vol.In(player_options),
vol.Required(CONF_MA_URL): TextSelector(TextSelectorConfig(type=TextSelectorType.URL)),
vol.Required(CONF_MA_TOKEN): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
vol.Optional(CONF_DEFAULT_TAB, default=DEFAULT_TAB): vol.In(
{
"player": "Player",
@@ -119,25 +142,67 @@ class MyMusicLibraryConfigFlow(ConfigFlow, domain=DOMAIN):
}
)
description_placeholders: dict[str, str] = {
"ma_url_hint": "e.g. http://homeassistant.local:8095",
}
if not ma_players:
description_placeholders["ma_warning"] = (
"Music Assistant integration not found — "
"install it first for full functionality."
)
return self.async_show_form(
step_id="user",
data_schema=schema,
errors=errors,
description_placeholders=description_placeholders,
description_placeholders={
"ma_url_hint": "e.g. http://homeassistant.local:8095",
"token_hint": (
"Generate a long-lived token in Music Assistant "
"(Settings → your user → API tokens) and paste it here."
),
},
)
async def async_step_reauth(
self, entry_data: dict[str, Any]
) -> ConfigFlowResult:
"""Handle reauth when the stored token is missing, revoked, or the URL changed."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Ask for a fresh URL + token and update the existing entry."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
url = user_input[CONF_MA_URL].rstrip("/")
token = user_input[CONF_MA_TOKEN]
if url_error := _validate_url(url):
errors[CONF_MA_URL] = url_error
else:
error = await _test_connection(self.hass, url, token)
if error:
errors["base"] = error
else:
return self.async_update_reload_and_abort(
reauth_entry,
data={**reauth_entry.data, CONF_MA_URL: url, CONF_MA_TOKEN: token},
)
schema = vol.Schema(
{
vol.Required(CONF_MA_URL, default=reauth_entry.data.get(CONF_MA_URL, "")): TextSelector(
TextSelectorConfig(type=TextSelectorType.URL)
),
vol.Required(CONF_MA_TOKEN): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=schema,
errors=errors,
)
class MyMusicLibraryOptionsFlow(OptionsFlow):
"""Handle options for My Music Library (player exclusion, etc.)."""
"""Handle options for My Music Library (default player, excluded players, debug)."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
@@ -147,6 +212,8 @@ class MyMusicLibraryOptionsFlow(OptionsFlow):
return self.async_create_entry(data=user_input)
all_players = _get_all_players(self.hass)
mml_players = _get_mml_players(self.hass)
current_excluded: list[str] = list(
self.config_entry.options.get(CONF_EXCLUDED_PLAYERS, [])
)
@@ -156,12 +223,22 @@ class MyMusicLibraryOptionsFlow(OptionsFlow):
if "*" in p or p in all_players
]
current_default_player = self.config_entry.options.get(CONF_DEFAULT_PLAYER, "")
if current_default_player not in mml_players:
current_default_player = ""
current_debug: bool = bool(
self.config_entry.options.get(CONF_DEBUG_MODE, False)
)
player_options = {
"": "Auto-detect (first available player)",
**mml_players,
}
schema = vol.Schema(
{
vol.Optional(CONF_DEFAULT_PLAYER, default=current_default_player): vol.In(player_options),
vol.Optional(CONF_EXCLUDED_PLAYERS, default=current_excluded): SelectSelector(
SelectSelectorConfig(
options=[{"value": k, "label": v} for k, v in all_players.items()],
+23 -2
View File
@@ -7,13 +7,14 @@ NAME = "My Music Library"
CONF_DEFAULT_PLAYER = "default_player"
CONF_DEFAULT_TAB = "default_tab"
CONF_MA_URL = "ma_url"
CONF_MA_TOKEN = "ma_token"
CONF_DEBUG_MODE = "debug_mode"
CONF_EXCLUDED_PLAYERS = "excluded_players"
DEFAULT_TAB = "player"
DEFAULT_MA_URL = "http://homeassistant.local:8095"
MUSIC_ASSISTANT_DOMAIN = "mass"
# Config entry schema version (see async_migrate_entry in __init__.py)
CONFIG_ENTRY_VERSION = 2
CARD_JS_FILENAME = "my-music-library-card.js"
CARD_URL = f"/my_music_library/{CARD_JS_FILENAME}"
@@ -21,3 +22,23 @@ ICON_URL = f"/{DOMAIN}/icon.png"
# WebSocket command exposed to the frontend card
WS_CONFIG_COMMAND = f"{DOMAIN}/config"
# WebSocket subscription streaming MA queue changes to the card (queue_push.py)
WS_SUBSCRIBE_QUEUE_COMMAND = f"{DOMAIN}/subscribe_queue"
SIGNAL_QUEUE_EVENT = f"{DOMAIN}_queue_event"
# Timeouts for the native Music Assistant connection (mass_connection.py)
MA_CONNECT_TIMEOUT = 10
MA_LISTEN_READY_TIMEOUT = 30
# Custom entity service on our own media_player platform (replaces mass's
# music_assistant.play_media — schema trimmed to what the card actually calls)
SERVICE_PLAY_MEDIA = "play_media"
ATTR_MEDIA_ID = "media_id"
ATTR_ENQUEUE = "enqueue"
ATTR_RADIO_MODE = "radio_mode"
SERVICE_TRANSFER_QUEUE = "transfer_queue"
ATTR_TARGETS = "targets"
# Extra state attribute the card's _getMaPlayers() uses to recognize our
# media_player entities in hass.states (same attribute name mass used to expose).
ATTR_MASS_PLAYER_ID = "mass_player_id"
@@ -1,13 +1,12 @@
{
"domain": "my_music_library",
"name": "My Music Library",
"after_dependencies": ["mass"],
"codeowners": ["@Patafoin"],
"config_flow": true,
"dependencies": ["frontend", "http", "lovelace"],
"documentation": "https://github.com/Patafoin/ha-my-music-library",
"iot_class": "local_push",
"issue_tracker": "https://github.com/Patafoin/ha-my-music-library/issues",
"requirements": [],
"version": "3.14.0"
"requirements": ["music-assistant-client>=1.3.3,<2"],
"version": "4.8.2"
}
@@ -0,0 +1,37 @@
play_media:
target:
entity:
integration: my_music_library
domain: media_player
fields:
media_id:
required: true
example: "library://playlist/16"
selector:
text:
enqueue:
selector:
select:
translation_key: enqueue
options:
- play
- replace
- next
- replace_next
- add
radio_mode:
selector:
boolean:
transfer_queue:
target:
entity:
integration: my_music_library
domain: media_player
fields:
targets:
required: true
selector:
entity:
integration: my_music_library
domain: media_player
multiple: true
@@ -3,22 +3,33 @@
"step": {
"user": {
"title": "Set up My Music Library",
"description": "Enter the URL of your Music Assistant server ({ma_url_hint}). Required for search and library browsing.",
"description": "Enter the URL of your Music Assistant server ({ma_url_hint}) and a long-lived API token ({token_hint}).",
"data": {
"ma_url": "Music Assistant server URL",
"default_player": "Default player device",
"ma_token": "Music Assistant API token",
"default_tab": "Default tab on open"
}
},
"reauth_confirm": {
"title": "Reconnect to Music Assistant",
"description": "My Music Library connects to Music Assistant directly (since version 4). Enter the server URL and a long-lived API token — create one in Music Assistant under Settings → your user → API tokens. This step also shows up when the stored token was revoked.",
"data": {
"ma_url": "Music Assistant server URL",
"ma_token": "Music Assistant API token"
}
}
},
"error": {
"cannot_connect": "Failed to connect",
"cannot_connect": "Failed to connect to the Music Assistant server",
"invalid_auth": "Authentication failed — check your API token",
"invalid_server_version": "This Music Assistant server version is not compatible",
"invalid_url": "Invalid URL — must start with http:// or https://",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "My Music Library is already configured",
"single_instance_allowed": "Only a single instance is allowed"
"single_instance_allowed": "Only a single instance is allowed",
"reauth_successful": "Reconnected to Music Assistant successfully"
}
},
"options": {
@@ -27,13 +38,74 @@
"title": "Player settings",
"description": "Select players to hide from the device picker. You can also type a wildcard pattern (e.g. `media_player.browser_mod_*`) and press Enter to exclude all matching players.",
"data": {
"default_player": "Default player device",
"excluded_players": "Hidden players",
"debug_mode": "Enable debug logging"
},
"data_description": {
"debug_mode": "When enabled, detailed logs appear in HA logs (filter: my_music_library) and in the browser console (F12). Disable after debugging."
"debug_mode": "When enabled, detailed logs appear in HA logs (filter: my_music_library) and in the browser console (F12). When disabled, the level set by `logger:` in configuration.yaml applies (HA default: warning). Disable after debugging."
}
}
}
},
"issues": {
"duplicate_entities": {
"title": "Duplicate Music Assistant players",
"fix_flow": {
"step": {
"confirm": {
"title": "Give the original IDs to My Music Library",
"description": "These entities will be renamed (My Music Library entity → original ID):\n\n{entities}\n\nThe official entities get the `_music_assistant` suffix. Both renames can be undone in each entity's settings.\n\n**Automations and scripts referencing the original IDs** (they will drive the My Music Library entities from now on):\n\n{references}\n\nStandard `media_player.*` actions keep working. Actions of the official integration, such as `music_assistant.play_media`, will no longer work on these entities: replace them with `my_music_library.play_media` or `media_player.play_media`.",
"data": {
"disable_official": "Also disable the official integration's entities"
}
}
},
"abort": {
"no_duplicates": "No duplicate left: nothing to rename."
}
}
}
},
"services": {
"play_media": {
"name": "Play media",
"description": "Plays a Music Assistant item (URI, URL or name) on a My Music Library player.",
"fields": {
"media_id": {
"name": "Media",
"description": "URI of a Music Assistant item (e.g. library://playlist/16), a stream URL, or a name to look up."
},
"enqueue": {
"name": "Enqueue",
"description": "How to add the media to the queue. Default: play now."
},
"radio_mode": {
"name": "Radio mode",
"description": "Keep playing similar tracks once the queue is finished."
}
}
},
"transfer_queue": {
"name": "Transfer queue",
"description": "Moves what a My Music Library player is playing (same track, same position) to one or more other players. With several targets, the first one leads a group of them all; the source stops.",
"fields": {
"targets": {
"name": "Targets",
"description": "Players that take over the music."
}
}
}
},
"selector": {
"enqueue": {
"options": {
"play": "Play now",
"replace": "Replace the queue and play",
"next": "Play next",
"replace_next": "Replace the queue, play next",
"add": "Add to the queue"
}
}
}
}
@@ -3,22 +3,33 @@
"step": {
"user": {
"title": "My Music Library einrichten",
"description": "Geben Sie die URL Ihres Music Assistant Servers ein ({ma_url_hint}). Erforderlich für Suche und Bibliotheksdurchsuchung.",
"description": "Geben Sie die URL Ihres Music Assistant Servers ({ma_url_hint}) und ein langlebiges API-Token ({token_hint}) ein.",
"data": {
"ma_url": "URL des Music Assistant Servers",
"default_player": "Standard-Player",
"ma_token": "Music Assistant API-Token",
"default_tab": "Standardreiter beim Öffnen"
}
},
"reauth_confirm": {
"title": "Erneut mit Music Assistant verbinden",
"description": "My Music Library verbindet sich direkt mit Music Assistant (seit Version 4). Gib die Server-URL und ein langlebiges API-Token ein – erstellbar in Music Assistant unter Einstellungen → dein Benutzer → API-Tokens. Dieser Schritt erscheint auch, wenn das gespeicherte Token widerrufen wurde.",
"data": {
"ma_url": "URL des Music Assistant Servers",
"ma_token": "Music Assistant API-Token"
}
}
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
"cannot_connect": "Verbindung zum Music Assistant Server fehlgeschlagen",
"invalid_auth": "Authentifizierung fehlgeschlagen — API-Token prüfen",
"invalid_server_version": "Diese Music Assistant Serverversion ist nicht kompatibel",
"invalid_url": "Ungültige URL — muss mit http:// oder https:// beginnen",
"unknown": "Unerwarteter Fehler"
},
"abort": {
"already_configured": "My Music Library ist bereits konfiguriert",
"single_instance_allowed": "Nur eine Instanz ist erlaubt"
"single_instance_allowed": "Nur eine Instanz ist erlaubt",
"reauth_successful": "Erfolgreich erneut mit Music Assistant verbunden"
}
},
"options": {
@@ -27,13 +38,74 @@
"title": "Player-Einstellungen",
"description": "Wählen Sie Player aus, die in der Geräteauswahl ausgeblendet werden sollen. Sie können auch ein Wildcard-Muster eingeben (z.B. `media_player.browser_mod_*`) und Enter drücken, um alle passenden Player auszuschließen.",
"data": {
"default_player": "Standard-Player",
"excluded_players": "Ausgeblendete Player",
"debug_mode": "Debug-Protokollierung aktivieren"
},
"data_description": {
"debug_mode": "Wenn aktiviert, erscheinen detaillierte Protokolle in den HA-Logs (Filter: my_music_library) und in der Browser-Konsole (F12). Nach dem Debugging deaktivieren."
"debug_mode": "Wenn aktiviert, erscheinen detaillierte Protokolle in den HA-Logs (Filter: my_music_library) und in der Browser-Konsole (F12). Wenn deaktiviert, gilt die mit `logger:` in configuration.yaml festgelegte Stufe (HA-Standard: warning). Nach dem Debugging deaktivieren."
}
}
}
},
"issues": {
"duplicate_entities": {
"title": "Doppelte Music-Assistant-Player",
"fix_flow": {
"step": {
"confirm": {
"title": "Ursprüngliche IDs an My Music Library vergeben",
"description": "Diese Entitäten werden umbenannt (My-Music-Library-Entität → ursprüngliche ID):\n\n{entities}\n\nDie offiziellen Entitäten erhalten das Suffix `_music_assistant`. Beide Umbenennungen lassen sich in den Einstellungen jeder Entität rückgängig machen.\n\n**Automationen und Skripte, die die ursprünglichen IDs verwenden** (sie steuern ab jetzt die My-Music-Library-Entitäten):\n\n{references}\n\nStandard-Aktionen `media_player.*` funktionieren weiterhin. Aktionen der offiziellen Integration, z. B. `music_assistant.play_media`, funktionieren auf diesen Entitäten nicht mehr: ersetze sie durch `my_music_library.play_media` oder `media_player.play_media`.",
"data": {
"disable_official": "Entitäten der offiziellen Integration zusätzlich deaktivieren"
}
}
},
"abort": {
"no_duplicates": "Keine Duplikate mehr: nichts umzubenennen."
}
}
}
},
"services": {
"play_media": {
"name": "Medien abspielen",
"description": "Spielt ein Music-Assistant-Element (URI, URL oder Name) auf einem My-Music-Library-Player ab.",
"fields": {
"media_id": {
"name": "Medium",
"description": "URI eines Music-Assistant-Elements (z. B. library://playlist/16), eine Stream-URL oder ein zu suchender Name."
},
"enqueue": {
"name": "Einreihen",
"description": "Wie das Medium zur Warteschlange hinzugefügt wird. Standard: sofort abspielen."
},
"radio_mode": {
"name": "Radiomodus",
"description": "Nach dem Ende der Warteschlange ähnliche Titel weiterspielen."
}
}
},
"transfer_queue": {
"name": "Warteschlange übertragen",
"description": "Überträgt, was ein My-Music-Library-Player abspielt (gleicher Titel, gleiche Position), auf einen oder mehrere andere Player. Bei mehreren Zielen führt das erste eine Gruppe aus allen an; die Quelle stoppt.",
"fields": {
"targets": {
"name": "Ziele",
"description": "Player, die die Musik übernehmen."
}
}
}
},
"selector": {
"enqueue": {
"options": {
"play": "Jetzt abspielen",
"replace": "Warteschlange ersetzen und abspielen",
"next": "Als Nächstes abspielen",
"replace_next": "Warteschlange ersetzen, als Nächstes abspielen",
"add": "Zur Warteschlange hinzufügen"
}
}
}
}
@@ -3,22 +3,33 @@
"step": {
"user": {
"title": "Set up My Music Library",
"description": "Enter the URL of your Music Assistant server ({ma_url_hint}). Required for search and library browsing.",
"description": "Enter the URL of your Music Assistant server ({ma_url_hint}) and a long-lived API token ({token_hint}).",
"data": {
"ma_url": "Music Assistant server URL",
"default_player": "Default player device",
"ma_token": "Music Assistant API token",
"default_tab": "Default tab on open"
}
},
"reauth_confirm": {
"title": "Reconnect to Music Assistant",
"description": "My Music Library connects to Music Assistant directly (since version 4). Enter the server URL and a long-lived API token — create one in Music Assistant under Settings → your user → API tokens. This step also shows up when the stored token was revoked.",
"data": {
"ma_url": "Music Assistant server URL",
"ma_token": "Music Assistant API token"
}
}
},
"error": {
"cannot_connect": "Failed to connect",
"cannot_connect": "Failed to connect to the Music Assistant server",
"invalid_auth": "Authentication failed — check your API token",
"invalid_server_version": "This Music Assistant server version is not compatible",
"invalid_url": "Invalid URL — must start with http:// or https://",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "My Music Library is already configured",
"single_instance_allowed": "Only a single instance is allowed"
"single_instance_allowed": "Only a single instance is allowed",
"reauth_successful": "Reconnected to Music Assistant successfully"
}
},
"options": {
@@ -27,13 +38,74 @@
"title": "Player settings",
"description": "Select players to hide from the device picker. You can also type a wildcard pattern (e.g. `media_player.browser_mod_*`) and press Enter to exclude all matching players.",
"data": {
"default_player": "Default player device",
"excluded_players": "Hidden players",
"debug_mode": "Enable debug logging"
},
"data_description": {
"debug_mode": "When enabled, detailed logs appear in HA logs (filter: my_music_library) and in the browser console (F12). Disable after debugging."
"debug_mode": "When enabled, detailed logs appear in HA logs (filter: my_music_library) and in the browser console (F12). When disabled, the level set by `logger:` in configuration.yaml applies (HA default: warning). Disable after debugging."
}
}
}
},
"issues": {
"duplicate_entities": {
"title": "Duplicate Music Assistant players",
"fix_flow": {
"step": {
"confirm": {
"title": "Give the original IDs to My Music Library",
"description": "These entities will be renamed (My Music Library entity → original ID):\n\n{entities}\n\nThe official entities get the `_music_assistant` suffix. Both renames can be undone in each entity's settings.\n\n**Automations and scripts referencing the original IDs** (they will drive the My Music Library entities from now on):\n\n{references}\n\nStandard `media_player.*` actions keep working. Actions of the official integration, such as `music_assistant.play_media`, will no longer work on these entities: replace them with `my_music_library.play_media` or `media_player.play_media`.",
"data": {
"disable_official": "Also disable the official integration's entities"
}
}
},
"abort": {
"no_duplicates": "No duplicate left: nothing to rename."
}
}
}
},
"services": {
"play_media": {
"name": "Play media",
"description": "Plays a Music Assistant item (URI, URL or name) on a My Music Library player.",
"fields": {
"media_id": {
"name": "Media",
"description": "URI of a Music Assistant item (e.g. library://playlist/16), a stream URL, or a name to look up."
},
"enqueue": {
"name": "Enqueue",
"description": "How to add the media to the queue. Default: play now."
},
"radio_mode": {
"name": "Radio mode",
"description": "Keep playing similar tracks once the queue is finished."
}
}
},
"transfer_queue": {
"name": "Transfer queue",
"description": "Moves what a My Music Library player is playing (same track, same position) to one or more other players. With several targets, the first one leads a group of them all; the source stops.",
"fields": {
"targets": {
"name": "Targets",
"description": "Players that take over the music."
}
}
}
},
"selector": {
"enqueue": {
"options": {
"play": "Play now",
"replace": "Replace the queue and play",
"next": "Play next",
"replace_next": "Replace the queue, play next",
"add": "Add to the queue"
}
}
}
}
@@ -3,22 +3,33 @@
"step": {
"user": {
"title": "Configurer My Music Library",
"description": "Entrez l'URL de votre serveur Music Assistant ({ma_url_hint}). Requis pour la recherche et la navigation dans la bibliothèque.",
"description": "Entrez l'URL de votre serveur Music Assistant ({ma_url_hint}) et un token d'API longue durée ({token_hint}).",
"data": {
"ma_url": "URL du serveur Music Assistant",
"default_player": "Lecteur par défaut",
"ma_token": "Token API Music Assistant",
"default_tab": "Onglet affiché à l'ouverture"
}
},
"reauth_confirm": {
"title": "Reconnexion à Music Assistant",
"description": "My Music Library se connecte directement à Music Assistant (depuis la version 4). Entrez l'URL du serveur et un token d'API longue durée — à créer dans Music Assistant, Paramètres → votre utilisateur → Tokens d'API. Cette étape apparaît aussi quand le token enregistré a été révoqué.",
"data": {
"ma_url": "URL du serveur Music Assistant",
"ma_token": "Token API Music Assistant"
}
}
},
"error": {
"cannot_connect": "Connexion impossible",
"cannot_connect": "Connexion au serveur Music Assistant impossible",
"invalid_auth": "Authentification échouée — vérifiez votre token API",
"invalid_server_version": "Cette version du serveur Music Assistant n'est pas compatible",
"invalid_url": "URL invalide — doit commencer par http:// ou https://",
"unknown": "Erreur inattendue"
},
"abort": {
"already_configured": "My Music Library est déjà configuré",
"single_instance_allowed": "Une seule instance est autorisée"
"single_instance_allowed": "Une seule instance est autorisée",
"reauth_successful": "Reconnexion à Music Assistant réussie"
}
},
"options": {
@@ -27,13 +38,74 @@
"title": "Paramètres des lecteurs",
"description": "Sélectionnez les lecteurs à masquer dans le sélecteur de périphérique. Vous pouvez aussi saisir un motif avec wildcard (ex. `media_player.browser_mod_*`) et appuyer sur Entrée pour exclure tous les lecteurs correspondants.",
"data": {
"default_player": "Lecteur par défaut",
"excluded_players": "Lecteurs masqués",
"debug_mode": "Activer les logs de débogage"
},
"data_description": {
"debug_mode": "Quand activé, des logs détaillés apparaissent dans les journaux HA (filtre : my_music_library) et dans la console du navigateur (F12). Désactivez après le débogage."
"debug_mode": "Quand activé, des logs détaillés apparaissent dans les journaux HA (filtre : my_music_library) et dans la console du navigateur (F12). Quand désactivé, c'est le niveau défini par `logger:` dans configuration.yaml qui s'applique (par défaut dans HA : warning). Désactivez après le débogage."
}
}
}
},
"issues": {
"duplicate_entities": {
"title": "Lecteurs Music Assistant en double",
"fix_flow": {
"step": {
"confirm": {
"title": "Donner les identifiants d'origine à My Music Library",
"description": "Ces entités vont être renommées (entité My Music Library → identifiant d'origine) :\n\n{entities}\n\nLes entités officielles reçoivent le suffixe `_music_assistant`. Les deux renommages peuvent être annulés dans les paramètres de chaque entité.\n\n**Automatisations et scripts qui utilisent les identifiants d'origine** (ils piloteront désormais les entités My Music Library) :\n\n{references}\n\nLes actions standard `media_player.*` continuent de fonctionner. Les actions de l'intégration officielle, comme `music_assistant.play_media`, ne fonctionneront plus sur ces entités : remplacez-les par `my_music_library.play_media` ou `media_player.play_media`.",
"data": {
"disable_official": "Désactiver aussi les entités de l'intégration officielle"
}
}
},
"abort": {
"no_duplicates": "Plus aucun doublon : rien à renommer."
}
}
}
},
"services": {
"play_media": {
"name": "Lire un média",
"description": "Lit un élément Music Assistant (URI, URL ou nom) sur un lecteur My Music Library.",
"fields": {
"media_id": {
"name": "Média",
"description": "URI d'un élément Music Assistant (ex. library://playlist/16), URL d'un flux, ou nom à rechercher."
},
"enqueue": {
"name": "Mise en file",
"description": "Comment ajouter le média à la file d'attente. Par défaut : lecture immédiate."
},
"radio_mode": {
"name": "Mode radio",
"description": "Continuer avec des titres similaires une fois la file terminée."
}
}
},
"transfer_queue": {
"name": "Transférer la file d'attente",
"description": "Déplace ce que joue un lecteur My Music Library (même titre, même position) vers un ou plusieurs autres lecteurs. Avec plusieurs destinations, la première mène un groupe qui les réunit toutes ; la source s'arrête.",
"fields": {
"targets": {
"name": "Destinations",
"description": "Lecteurs qui reprennent la musique."
}
}
}
},
"selector": {
"enqueue": {
"options": {
"play": "Lire maintenant",
"replace": "Remplacer la file et lire",
"next": "Lire ensuite",
"replace_next": "Remplacer la file, lire ensuite",
"add": "Ajouter à la file"
}
}
}
}
File diff suppressed because it is too large. Load diff