Added Alexa Music
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
"""My Music Library — Home Assistant Integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.components.websocket_api import (
|
||||
ActiveConnection,
|
||||
async_register_command,
|
||||
websocket_command,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.loader import async_get_integration
|
||||
|
||||
from .api import ImageProxyView, 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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[str] = []
|
||||
|
||||
WWW_DIR = os.path.join(os.path.dirname(__file__), "www")
|
||||
ICON_PATH = os.path.join(os.path.dirname(__file__), "brand", "icon.png")
|
||||
|
||||
_QUEUE_STORE_KEY = f"{DOMAIN}_queues"
|
||||
_QUEUE_STORE_VERSION = 1
|
||||
|
||||
_INTEGRATION_LOGGER = logging.getLogger("custom_components.my_music_library")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
"""Set up the My Music Library component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
# Load persisted per-player queues from disk
|
||||
store = Store(hass, _QUEUE_STORE_VERSION, _QUEUE_STORE_KEY)
|
||||
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", {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up My Music Library from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# Serve the card JS file from /my_music_library/<filename>
|
||||
card_js_path = os.path.join(WWW_DIR, CARD_JS_FILENAME)
|
||||
if not os.path.isfile(card_js_path):
|
||||
_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)
|
||||
registered_paths: set[str] = hass.data[DOMAIN].setdefault("_registered_paths", set())
|
||||
|
||||
static_registrations: list[StaticPathConfig] = []
|
||||
if CARD_URL not in registered_paths:
|
||||
static_registrations.append(StaticPathConfig(CARD_URL, card_js_path, cache_headers=False))
|
||||
registered_paths.add(CARD_URL)
|
||||
_LOGGER.debug("Registered static path %s -> %s", CARD_URL, card_js_path)
|
||||
else:
|
||||
_LOGGER.debug("Static path already registered, skipping: %s", CARD_URL)
|
||||
|
||||
if ICON_URL not in registered_paths and os.path.isfile(ICON_PATH):
|
||||
static_registrations.append(StaticPathConfig(ICON_URL, ICON_PATH, cache_headers=False))
|
||||
registered_paths.add(ICON_URL)
|
||||
_LOGGER.debug("Registered icon static path %s -> %s", ICON_URL, ICON_PATH)
|
||||
|
||||
if static_registrations:
|
||||
await hass.http.async_register_static_paths(static_registrations)
|
||||
|
||||
# Build a versioned URL for reliable browser cache-busting, same principle as
|
||||
# HACS's ?hacstag= parameter.
|
||||
#
|
||||
# We deliberately do NOT use add_extra_js_url: that mechanism loads the module
|
||||
# independently of the Lovelace resource, and HA's scoped-custom-element-registry
|
||||
# polyfill causes customElements.define to be called twice even when both paths
|
||||
# use the same URL — triggering "already been used with this registry" errors.
|
||||
# The Lovelace resource mechanism is the standard approach for custom cards and
|
||||
# is sufficient (lovelace is a hard dependency so registration is guaranteed).
|
||||
integration = await async_get_integration(hass, DOMAIN)
|
||||
version = integration.manifest.get("version", "0")
|
||||
versioned_card_url = f"{CARD_URL}?v={version}"
|
||||
_LOGGER.debug("Setting up My Music Library v%s", version)
|
||||
|
||||
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)
|
||||
|
||||
# Register WebSocket command so the card can fetch its config
|
||||
_register_websocket_commands(hass)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {"entry": entry}
|
||||
|
||||
_apply_debug_mode(entry.options.get(CONF_DEBUG_MODE, False))
|
||||
entry.async_on_unload(entry.add_update_listener(_async_options_updated))
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _async_options_updated(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> 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:
|
||||
"""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)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
def _register_websocket_commands(hass: HomeAssistant) -> None:
|
||||
"""Register WebSocket commands exposed to the frontend card."""
|
||||
|
||||
@websocket_command({vol.Required("type"): WS_CONFIG_COMMAND})
|
||||
def ws_get_config(
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||
) -> 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})
|
||||
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"],
|
||||
{
|
||||
"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"),
|
||||
"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 def _async_register_lovelace_resource(
|
||||
hass: HomeAssistant, url: str, base_url: str
|
||||
) -> None:
|
||||
"""Add the card JS as a Lovelace resource (for Cast / companion app support).
|
||||
|
||||
``url`` — the versioned URL to register (e.g. /my_music_library/card.js?v=3.1.2)
|
||||
``base_url`` — the fixed base path without query params (e.g. /my_music_library/card.js)
|
||||
|
||||
Strategy — delete-then-add, never add-then-delete:
|
||||
1. Collect every existing Lovelace resource whose URL starts with ``base_url``
|
||||
(this matches the exact current URL, any previous versioned URL, and the
|
||||
plain unversioned URL used by 3.1.1).
|
||||
2. If the only existing entry is already the target ``url``, do nothing.
|
||||
3. Otherwise delete ALL collected entries first, then add the new ``url``.
|
||||
|
||||
Deleting before adding ensures the browser never sees two different module
|
||||
versions in Lovelace storage at the same time, which would cause
|
||||
customElements.define to be called twice → "configuration error".
|
||||
"""
|
||||
try:
|
||||
lovelace = hass.data.get("lovelace")
|
||||
if lovelace is None:
|
||||
return
|
||||
|
||||
if hasattr(lovelace, "resources"):
|
||||
resources = lovelace.resources
|
||||
elif isinstance(lovelace, dict):
|
||||
resources = lovelace.get("resources")
|
||||
else:
|
||||
return
|
||||
|
||||
if resources is None:
|
||||
return
|
||||
|
||||
await resources.async_load()
|
||||
|
||||
# Collect all existing entries that belong to this card.
|
||||
existing: list[tuple[str, str]] = [] # (item_id, r_url)
|
||||
for r in resources.async_items():
|
||||
r_url = r.get("url", "") if isinstance(r, dict) else getattr(r, "url", "")
|
||||
if r_url == base_url or r_url.startswith(base_url + "?"):
|
||||
item_id = r.get("id") if isinstance(r, dict) else getattr(r, "id", None)
|
||||
if item_id:
|
||||
existing.append((item_id, r_url))
|
||||
|
||||
# Already perfectly registered — nothing to do.
|
||||
if len(existing) == 1 and existing[0][1] == url:
|
||||
_LOGGER.debug("Lovelace resource already registered: %s", url)
|
||||
return
|
||||
|
||||
delete_fn = getattr(resources, "async_delete_item", None)
|
||||
create_fn = getattr(resources, "async_create_item", None)
|
||||
|
||||
# If we have stale entries but cannot delete them, bail out entirely.
|
||||
# Adding the new URL alongside a stale one would make the browser load
|
||||
# two different module versions → customElements.define conflict → error.
|
||||
if existing and not callable(delete_fn):
|
||||
_LOGGER.debug(
|
||||
"Cannot clean up stale Lovelace resource(s) — skipping registration"
|
||||
)
|
||||
return
|
||||
|
||||
# Delete ALL stale entries first.
|
||||
for item_id, old_url in existing:
|
||||
try:
|
||||
await delete_fn(item_id)
|
||||
_LOGGER.info("Removed old Lovelace resource %s (id=%s)", old_url, item_id)
|
||||
except Exception: # noqa: BLE001
|
||||
# A deletion failed: abort to avoid a stale + new entry coexisting.
|
||||
_LOGGER.warning(
|
||||
"Failed to remove Lovelace resource id=%s — aborting registration",
|
||||
item_id,
|
||||
)
|
||||
return
|
||||
|
||||
# Add the new versioned entry.
|
||||
if callable(create_fn):
|
||||
await create_fn({"res_type": "module", "url": url})
|
||||
_LOGGER.info("Lovelace resource registered: %s", url)
|
||||
else:
|
||||
# Fallback for very old HA builds without async_create_item.
|
||||
# Only reached when existing is empty (otherwise we returned above),
|
||||
# so there is no stale entry to collide with.
|
||||
data = getattr(resources, "data", None)
|
||||
if isinstance(data, list):
|
||||
if not any(
|
||||
(r.get("url") if isinstance(r, dict) else getattr(r, "url", "")) == url
|
||||
for r in data
|
||||
):
|
||||
data.append({"type": "module", "url": url})
|
||||
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.debug("Lovelace resource registration skipped for %s (non-critical)", url)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,177 @@
|
||||
"""Config flow for My Music Library."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
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.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_DEBUG_MODE,
|
||||
CONF_DEFAULT_PLAYER,
|
||||
CONF_DEFAULT_TAB,
|
||||
CONF_EXCLUDED_PLAYERS,
|
||||
CONF_MA_URL,
|
||||
DEFAULT_MA_URL,
|
||||
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."""
|
||||
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
|
||||
):
|
||||
state = hass.states.get(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."""
|
||||
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
|
||||
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:
|
||||
return "invalid_url"
|
||||
except Exception: # noqa: BLE001
|
||||
return "invalid_url"
|
||||
return None
|
||||
|
||||
|
||||
class MyMusicLibraryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for My Music Library."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
"""Return the options flow handler."""
|
||||
return MyMusicLibraryOptionsFlow()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
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:
|
||||
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,
|
||||
}
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MA_URL, default=DEFAULT_MA_URL): str,
|
||||
vol.Optional(CONF_DEFAULT_PLAYER, default=""): vol.In(player_options),
|
||||
vol.Optional(CONF_DEFAULT_TAB, default=DEFAULT_TAB): vol.In(
|
||||
{
|
||||
"player": "Player",
|
||||
"search": "Search",
|
||||
"library": "Library",
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class MyMusicLibraryOptionsFlow(OptionsFlow):
|
||||
"""Handle options for My Music Library (player exclusion, etc.)."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
all_players = _get_all_players(self.hass)
|
||||
current_excluded: list[str] = list(
|
||||
self.config_entry.options.get(CONF_EXCLUDED_PLAYERS, [])
|
||||
)
|
||||
# Keep wildcard patterns as-is; drop stale exact entity IDs only.
|
||||
current_excluded = [
|
||||
p for p in current_excluded
|
||||
if "*" in p or p in all_players
|
||||
]
|
||||
|
||||
current_debug: bool = bool(
|
||||
self.config_entry.options.get(CONF_DEBUG_MODE, False)
|
||||
)
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_EXCLUDED_PLAYERS, default=current_excluded): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[{"value": k, "label": v} for k, v in all_players.items()],
|
||||
multiple=True,
|
||||
mode=SelectSelectorMode.LIST,
|
||||
custom_value=True,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_DEBUG_MODE, default=current_debug): bool,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=schema)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Constants for My Music Library."""
|
||||
from __future__ import annotations
|
||||
|
||||
DOMAIN = "my_music_library"
|
||||
NAME = "My Music Library"
|
||||
|
||||
CONF_DEFAULT_PLAYER = "default_player"
|
||||
CONF_DEFAULT_TAB = "default_tab"
|
||||
CONF_MA_URL = "ma_url"
|
||||
CONF_DEBUG_MODE = "debug_mode"
|
||||
CONF_EXCLUDED_PLAYERS = "excluded_players"
|
||||
|
||||
DEFAULT_TAB = "player"
|
||||
DEFAULT_MA_URL = "http://homeassistant.local:8095"
|
||||
|
||||
MUSIC_ASSISTANT_DOMAIN = "mass"
|
||||
|
||||
CARD_JS_FILENAME = "my-music-library-card.js"
|
||||
CARD_URL = f"/my_music_library/{CARD_JS_FILENAME}"
|
||||
ICON_URL = f"/{DOMAIN}/icon.png"
|
||||
|
||||
# WebSocket command exposed to the frontend card
|
||||
WS_CONFIG_COMMAND = f"{DOMAIN}/config"
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||
<!-- Black rounded background -->
|
||||
<rect width="256" height="256" rx="40" fill="#111111"/>
|
||||
<!-- Double croche (beamed sixteenth notes) in green -->
|
||||
<g fill="#22c55e">
|
||||
<!-- Note head 1 (left) -->
|
||||
<ellipse cx="89" cy="173" rx="25" ry="18" transform="rotate(-15 89 173)"/>
|
||||
<!-- Note head 2 (right) -->
|
||||
<ellipse cx="166" cy="155" rx="25" ry="18" transform="rotate(-15 166 155)"/>
|
||||
<!-- Stem 1 -->
|
||||
<rect x="109" y="80" width="9" height="96"/>
|
||||
<!-- Stem 2 -->
|
||||
<rect x="186" y="62" width="9" height="96"/>
|
||||
<!-- Beam 1 (upper) -->
|
||||
<polygon points="109,80 195,62 195,76 109,94"/>
|
||||
<!-- Beam 2 (lower — makes it a double croche) -->
|
||||
<polygon points="109,101 195,83 195,97 109,115"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 805 B |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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.10.4"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"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.",
|
||||
"data": {
|
||||
"ma_url": "Music Assistant server URL",
|
||||
"default_player": "Default player device",
|
||||
"default_tab": "Default tab on open"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"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": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"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.",
|
||||
"data": {
|
||||
"ma_url": "URL des Music Assistant Servers",
|
||||
"default_player": "Standard-Player",
|
||||
"default_tab": "Standardreiter beim Öffnen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"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": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"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.",
|
||||
"data": {
|
||||
"ma_url": "Music Assistant server URL",
|
||||
"default_player": "Default player device",
|
||||
"default_tab": "Default tab on open"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"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": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"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.",
|
||||
"data": {
|
||||
"ma_url": "URL du serveur Music Assistant",
|
||||
"default_player": "Lecteur par défaut",
|
||||
"default_tab": "Onglet affiché à l'ouverture"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Connexion impossible",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"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": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user