ConfigValueOption("debug", "DEBUG"),
),
default_value="GLOBAL",
- description="Set the log verbosity for this provider",
advanced=True,
)
type=ConfigEntryType.BOOLEAN,
label="Enable queue flow mode",
default_value=False,
- description='Enable "flow" mode where all queue tracks are sent as a continuous '
- "audio stream. \nUse for players that do not natively support gapless and/or "
- "crossfading or if the player has trouble transitioning between tracks.",
advanced=False,
)
],
default_value="stereo",
label="Output Channel Mode",
- description="You can configure this player to play only the left or right channel, "
- "for example to a create a stereo pair with 2 players.",
advanced=True,
)
CONF_ENTRY_VOLUME_NORMALIZATION = ConfigEntry(
key=CONF_VOLUME_NORMALIZATION,
type=ConfigEntryType.BOOLEAN,
- label="Enable volume normalization (EBU-R128 based)",
+ label="Enable volume normalization",
default_value=True,
- description="Enable volume normalization based on the EBU-R128 "
- "standard without affecting dynamic range",
+ description="Enable volume normalization (EBU-R128 based)",
)
CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry(
range=(-30, 0),
default_value=-17,
label="Target level for volume normalization",
- description="Adjust average (perceived) loudness to this target level, "
- "default is -17 LUFS \n\n WARNING: Setting levels higher than this may result in clipping",
+ description="Adjust average (perceived) loudness to this target level",
depends_on=CONF_VOLUME_NORMALIZATION,
advanced=True,
)
type=ConfigEntryType.BOOLEAN,
label="Hide this player in the user interface",
default_value=False,
- description="Hide this player in the user interface. \n\n"
- "Note that it can still be controlled and it will also show up in any "
- "sync groups the player belongs to.",
advanced=True,
)
@api_command("config/players")
async def get_player_configs(self, provider: str | None = None) -> list[PlayerConfig]:
"""Return all known player configurations, optionally filtered by provider domain."""
- available_providers = {x.domain for x in self.mass.providers}
+ available_providers = {x.instance_id for x in self.mass.providers}
return [
await self.get_player_config(player_id)
for player_id, raw_conf in self.get(CONF_PLAYERS, {}).items()
else:
raise KeyError(f"Unknown provider domain: {provider_domain}")
config_entries = await self.get_provider_config_entries(provider_domain)
+ instance_id = f"{manifest.domain}--{shortuuid.random(8)}"
default_config: ProviderConfig = ProviderConfig.parse(
config_entries,
{
"type": manifest.type.value,
"domain": manifest.domain,
- "instance_id": manifest.domain,
+ "instance_id": instance_id,
"name": manifest.name,
# note: this will only work for providers that do
# not have any required config entries or provide defaults
# determine instance id based on previous configs
if existing and not manifest.multi_instance:
raise ValueError(f"Provider {manifest.name} does not support multiple instances")
- if len(existing) == 0:
- instance_id = provider_domain
- name = manifest.name
- else:
- random_id = shortuuid.random(6)
- instance_id = f"{provider_domain}_{random_id}"
- name = f"{manifest.name} {random_id}"
+ instance_id = f"{manifest.domain}--{shortuuid.random(8)}"
# all checks passed, create config object
config_entries = await self.get_provider_config_entries(
provider_domain=provider_domain, instance_id=instance_id, values=values
"type": manifest.type.value,
"domain": manifest.domain,
"instance_id": instance_id,
- "name": name,
+ "name": manifest.name,
"values": values,
},
)
self._prev_states[queue_id] = new_state
return
# handle player was playing and is now stopped
- # if player finished playing a track for 90%, mark current item as finished
+ # if player finished playing a track for 85%, mark current item as finished
if (
prev_state.get("state") == "playing"
and queue.state == PlayerState.IDLE
and (
queue.current_item
and queue.current_item.duration
- and queue.elapsed_time > (queue.current_item.duration * 0.8)
+ and prev_state.get("elapsed_time", queue.elapsed_time)
+ > (queue.current_item.duration * 0.85)
)
):
queue.current_index += 1
# - every 30 seconds if the player is powered
# - every 10 seconds if the player is playing
if (
- player.available
+ (player.available or count == 360)
and (
(player.powered and count % 30 == 0)
or (player_playing and count % 10 == 0)
"contentFeatures.dlna.org": "DLNA.ORG_OP=00;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000", # noqa: E501
"Cache-Control": "no-cache",
"Connection": "close",
- # "Accept-Ranges": "none",
+ "Accept-Ranges": "none",
"icy-name": "Music Assistant",
"icy-pub": "0",
}
)
await resp.prepare(request)
- # return early if this is only a HEAD request
- if request.method == "HEAD":
+ # return early if this is not a GET request
+ if request.method != "GET":
return resp
# all checks passed, start streaming!
)
await resp.prepare(request)
- # return early if this is only a HEAD request
- if request.method == "HEAD":
+ # return early if this is not a GET request
+ if request.method != "GET":
return resp
# all checks passed, start streaming!
)
await resp.prepare(request)
- # return early if this is only a HEAD request
- if request.method == "HEAD":
+ # return early if this is not a GET request
+ if request.method != "GET":
return resp
# some players (e.g. dlna, sonos) misbehave and do multiple GET requests
from music_assistant.common.models.config_entries import (
CONF_ENTRY_CROSSFADE_DURATION,
+ CONF_ENTRY_FLOW_MODE,
ConfigEntry,
ConfigValueType,
)
from music_assistant.common.models.errors import PlayerUnavailableError
from music_assistant.common.models.player import DeviceInfo, Player
from music_assistant.common.models.queue_item import QueueItem
-from music_assistant.constants import CONF_CROSSFADE, CONF_LOG_LEVEL, CONF_PLAYERS, MASS_LOGO_ONLINE
+from music_assistant.constants import (
+ CONF_CROSSFADE,
+ CONF_FLOW_MODE,
+ CONF_LOG_LEVEL,
+ CONF_PLAYERS,
+ MASS_LOGO_ONLINE,
+)
from music_assistant.server.models.player_provider import PlayerProvider
from .helpers import CastStatusListener, ChromecastInfo
type=ConfigEntryType.BOOLEAN,
label="Enable crossfade",
default_value=False,
- description="Enable a crossfade transition between (queue) tracks. \n"
- "Note that Chromecast does not natively support crossfading so Music Assistant "
- "uses a 'flow mode' workaround for this at the cost of on-player metadata.",
+ description="Enable a crossfade transition between (queue) tracks. \n\n"
+ "Note that Cast does not natively support crossfading so you need to enable "
+ "the 'flow mode' workaround to use crossfading with Cast players.",
advanced=False,
+ depends_on=CONF_FLOW_MODE,
),
+ CONF_ENTRY_FLOW_MODE,
CONF_ENTRY_CROSSFADE_DURATION,
)
_patched_process_media_status_org(self, data)
for status_msg in data.get("status", []):
if items := status_msg.get("items"):
+ self.status.current_item_id = status_msg.get("currentItemId", 0)
self.status.items = items
- fade_in: Optionally fade in the item at playback start.
"""
castplayer = self.castplayers[player_id]
- # Google cast does not support crossfading so we use flow mode to provide this feature
- use_flow_mode = await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE)
+ use_flow_mode = await self.mass.config.get_player_config_value(
+ player_id, CONF_FLOW_MODE
+ ) or await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE)
url = await self.mass.streams.resolve_stream_url(
queue_item=queue_item,
output_codec=ContentType.FLAC,
output_codec=ContentType.FLAC,
)
next_item_id = None
- if (cast_queue_items := getattr(castplayer.cc.media_controller.status, "items")) and len(
- cast_queue_items
- ) > 1:
- next_item_id = cast_queue_items[-1]["itemId"]
+ status = castplayer.cc.media_controller.status
+ # lookup position of current track in cast queue
+ cast_current_item_id = getattr(status, "current_item_id", 0)
+ cast_queue_items = getattr(status, "items", [])
+ cur_item_found = False
+ for item in cast_queue_items:
+ if item["itemId"] == cast_current_item_id:
+ cur_item_found = True
+ continue
+ elif not cur_item_found:
+ continue
+ next_item_id = item["itemId"]
+ # check if the next queue item isn't already queued
if (
- cast_queue_items[-1].get("media", {}).get("customData", {}).get("queue_item_id")
+ item.get("media", {}).get("customData", {}).get("queue_item_id")
== queue_item.queue_item_id
):
return
media_controller = castplayer.cc.media_controller
queuedata["mediaSessionId"] = media_controller.status.media_session_id
self.mass.create_task(media_controller.send_message, queuedata, inc_session_id=True)
- self.logger.info(
+ self.logger.debug(
"Enqued next track (%s) to player %s",
queue_item.name if queue_item else url,
castplayer.player.display_name,
"""Handle updated MediaStatus."""
castplayer.logger.debug("Received media status update: %s", status.player_state)
# player state
+ castplayer.player.elapsed_time_last_updated = time.time()
if status.player_is_playing:
castplayer.player.state = PlayerState.PLAYING
+ castplayer.player.current_item_id = status.content_id
elif status.player_is_paused:
castplayer.player.state = PlayerState.PAUSED
+ castplayer.player.current_item_id = status.content_id
else:
castplayer.player.state = PlayerState.IDLE
+ castplayer.player.current_item_id = None
# elapsed time
castplayer.player.elapsed_time_last_updated = time.time()
+ castplayer.player.elapsed_time = status.adjusted_current_time
if status.player_is_playing:
castplayer.player.elapsed_time = status.adjusted_current_time
else:
castplayer.player.active_source = castplayer.cc.app_display_name
# current media
- castplayer.player.current_item_id = status.content_id
self.mass.loop.call_soon_threadsafe(self.mass.players.update, castplayer.player_id)
- # handle end of MA queue - reset current_item_id
- if (
- castplayer.player.state == PlayerState.IDLE
- and castplayer.player.current_item_id
- and (queue := self.mass.player_queues.get(castplayer.player_id))
- and queue.next_item is None
- ):
- castplayer.player.current_item_id = None
-
def on_new_connection_status(self, castplayer: CastPlayer, status: ConnectionStatus) -> None:
"""Handle updated ConnectionStatus."""
castplayer.logger.debug("Received connection status update - status: %s", status.status)
- seek_position: Optional seek to this position.
- fade_in: Optionally fade in the item at playback start.
"""
- # DLNA players do not support crossfading so we enforce flow mode to provide this feature
use_flow_mode = await self.mass.config.get_player_config_value(
player_id, CONF_FLOW_MODE
) or await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE)
didl_metadata = create_didl_metadata(self.mass, url, queue_item)
title = queue_item.name
await dlna_player.device.async_set_next_transport_uri(url, title, didl_metadata)
- self.logger.info(
+ self.logger.debug(
"Enqued next track (%s) to player %s",
title,
dlna_player.player.display_name,
[("InstanceID", 0), ("NextURI", url), ("NextURIMetaData", metadata)],
timeout=60,
)
- self.logger.info(
+ self.logger.debug(
"Enqued next track (%s) to player %s",
queue_item.name if queue_item else url,
sonos_player.soco_device.player_name,