You must run the docker container with host network mode and the data volume is `/data`.
If you want access to your local music files from within MA, make sure to also mount that, e.g. /media.
-Note that accessing remote (SMB) shares can be done from within MA itself using the SMB File provider (but requires the priviliged flag).
+Note that accessing remote (SMB) shares can be done from within MA itself using the SMB File provider (but requires the privileged flag).
____________
hass_options = {}
log_level = hass_options.get("log_level", args.log_level).upper()
+ dev_mode = bool(os.environ.get("PYTHONDEVMODE", "0"))
# setup logger
logger = setup_logger(data_dir, log_level)
async def start_mass():
loop = asyncio.get_running_loop()
- if log_level == "DEBUG":
+ if dev_mode:
loop.set_debug(True)
await mass.start()
CONF_FLOW_MODE,
CONF_LOG_LEVEL,
CONF_OUTPUT_CHANNELS,
+ CONF_OUTPUT_CODEC,
CONF_VOLUME_NORMALISATION,
CONF_VOLUME_NORMALISATION_TARGET,
SECURE_STRING_SUBSTITUTE,
advanced=True,
),
)
+
+CONF_ENTRY_OUTPUT_CODEC = ConfigEntry(
+ key=CONF_OUTPUT_CODEC,
+ type=ConfigEntryType.STRING,
+ label="Output codec",
+ options=[
+ ConfigValueOption("FLAC (lossless, compact file size)", "flac"),
+ ConfigValueOption("M4A AAC (lossy, superior quality)", "aac"),
+ ConfigValueOption("MP3 (lossy, average quality)", "mp3"),
+ ConfigValueOption("WAV (lossless, huge file size)", "wav"),
+ ],
+ default_value="flac",
+ description="Define the codec that is sent to the player when streaming audio. "
+ "By default Music Assistant prefers FLAC because it is lossless, has a "
+ "respectable filesize and is supported by most player devices. "
+ "Change this setting only if needed for your device/environment.",
+ advanced=True,
+)
SEEK = "seek"
SET_MEMBERS = "set_members"
QUEUE = "queue"
+ CROSSFADE = "crossfade"
class EventType(StrEnum):
CONF_FLOW_MODE: Final[str] = "flow_mode"
CONF_LOG_LEVEL: Final[str] = "log_level"
CONF_HIDE_GROUP_CHILDS: Final[str] = "hide_group_childs"
+CONF_OUTPUT_CODEC: Final[str] = "output_codec"
# config default values
DEFAULT_HOST: Final[str] = "0.0.0.0"
if not force_refresh and (cache := await self.mass.cache.get(cache_key)):
return self.item_cls.from_dict(cache)
if provider := self.mass.get_provider(provider_instance_id_or_domain): # noqa: SIM102
- if item := await provider.get_item(self.media_type, item_id):
+ item: MediaItemType = None
+ try:
+ item = await provider.get_item(self.media_type, item_id)
+ except MediaNotFoundError:
+ # fallback to domain matching
+ for provider in self.mass.music.providers:
+ if not provider.available:
+ continue
+ if provider_instance_id_or_domain != provider.domain:
+ continue
+ with suppress(MediaNotFoundError):
+ if item := await provider.get_item(self.media_type, item_id):
+ break
+ if item:
await self.mass.cache.set(cache_key, item.to_dict())
return item
raise MediaNotFoundError(
continue
return await self._get_provider_dynamic_tracks(
prov_mapping.item_id,
- provider_instance_id_or_domain,
+ prov_mapping.provider_instance,
limit=limit,
)
# Fallback to the default implementation
"-i",
"-",
]
- # output args
- output_args = [
- # output args
- "-f",
- output_format.value,
+ input_args += ["-metadata", 'title="Music Assistant"']
+ # select output args
+ if output_format == ContentType.FLAC:
+ output_args = ["-f", "flac", "-compression_level", "3"]
+ elif output_format == ContentType.AAC:
+ output_args = ["-f", "adts", "-c:a", output_format.value, "-b:a", "320k"]
+ elif output_format == ContentType.MP3:
+ output_args = ["-f", "mp3", "-c:a", output_format.value, "-b:a", "320k"]
+ else:
+ output_args = ["-f", output_format.value]
+
+ output_args += [
+ # append channels
"-ac",
"1" if conf_channels != "stereo" else "2",
+ # append sample rate
"-ar",
str(output_sample_rate),
- "-compression_level",
- "0",
+ # output = pipe
"-",
]
# collect extra and filter args
if self.closed or self._proc.stdin.is_closing():
return
self._proc.stdin.write(data)
- await self._proc.stdin.drain()
+ with suppress(BrokenPipeError):
+ await self._proc.stdin.drain()
def write_eof(self) -> None:
"""Write end of file to to process stdin."""
from pychromecast.models import CastInfo
from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED
-from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueOption
+from music_assistant.common.models.config_entries import (
+ CONF_ENTRY_OUTPUT_CODEC,
+ ConfigEntry,
+ ConfigValueOption,
+)
from music_assistant.common.models.enums import (
ConfigEntryType,
ContentType,
from music_assistant.common.models.errors import PlayerUnavailableError, QueueEmpty
from music_assistant.common.models.player import DeviceInfo, Player
from music_assistant.common.models.queue_item import QueueItem
-from music_assistant.constants import CONF_HIDE_GROUP_CHILDS, CONF_PLAYERS, MASS_LOGO_ONLINE
+from music_assistant.constants import (
+ CONF_HIDE_GROUP_CHILDS,
+ CONF_OUTPUT_CODEC,
+ CONF_PLAYERS,
+ MASS_LOGO_ONLINE,
+)
from music_assistant.server.models.player_provider import PlayerProvider
from .helpers import CastStatusListener, ChromecastInfo
CONF_ALT_APP = "alt_app"
+
BASE_PLAYER_CONFIG_ENTRIES = (
ConfigEntry(
key=CONF_ALT_APP,
"the playback experience but may not work on non-Google hardware.",
advanced=True,
),
+ CONF_ENTRY_OUTPUT_CODEC,
)
) -> None:
"""Send PLAY MEDIA command to given player."""
castplayer = self.castplayers[player_id]
+ output_codec = self.mass.config.get_player_config_value(player_id, CONF_OUTPUT_CODEC).value
url = await self.mass.streams.resolve_stream_url(
queue_item=queue_item,
player_id=player_id,
seek_position=seek_position,
fade_in=fade_in,
- # prefer FLAC as it seems to work on all CC players
- content_type=ContentType.FLAC,
+ content_type=ContentType(output_codec),
flow_mode=flow_mode,
)
castplayer.flow_mode_active = flow_mode
from async_upnp_client.search import async_search
from async_upnp_client.utils import CaseInsensitiveDict
-from music_assistant.common.models.config_entries import ConfigEntry
+from music_assistant.common.models.config_entries import CONF_ENTRY_OUTPUT_CODEC, ConfigEntry
from music_assistant.common.models.enums import ContentType, PlayerFeature, PlayerState, PlayerType
from music_assistant.common.models.errors import PlayerUnavailableError, QueueEmpty
from music_assistant.common.models.player import DeviceInfo, Player
from music_assistant.common.models.queue_item import QueueItem
-from music_assistant.constants import CONF_PLAYERS
+from music_assistant.constants import CONF_OUTPUT_CODEC, CONF_PLAYERS
from music_assistant.server.helpers.didl_lite import create_didl_metadata
from music_assistant.server.models.player_provider import PlayerProvider
PlayerFeature.VOLUME_MUTE,
PlayerFeature.VOLUME_SET,
)
-PLAYER_CONFIG_ENTRIES = tuple() # we don't have any player config entries (for now)
+PLAYER_CONFIG_ENTRIES = (CONF_ENTRY_OUTPUT_CODEC,)
_DLNAPlayerProviderT = TypeVar("_DLNAPlayerProviderT", bound="DLNAPlayerProvider")
_R = TypeVar("_R")
for dlna_player in self.dlnaplayers.values():
tg.create_task(self._device_disconnect(dlna_player))
+ def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: # noqa: ARG002
+ """Return all (provider/player specific) Config Entries for the given player (if any)."""
+ return PLAYER_CONFIG_ENTRIES
+
def on_player_config_changed(
self, config: PlayerConfig, changed_keys: set[str] # noqa: ARG002
) -> None:
# always clear queue (by sending stop) first
if dlna_player.device.can_stop:
await self.cmd_stop(player_id)
-
+ output_codec = self.mass.config.get_player_config_value(player_id, CONF_OUTPUT_CODEC).value
url = await self.mass.streams.resolve_stream_url(
queue_item=queue_item,
player_id=dlna_player.udn,
seek_position=seek_position,
fade_in=fade_in,
- content_type=ContentType.FLAC,
+ content_type=ContentType(output_codec),
flow_mode=flow_mode,
)
return
# send queue item to dlna queue
+ output_codec = self.mass.config.get_player_config_value(
+ dlna_player.player.player_id, CONF_OUTPUT_CODEC
+ ).value
url = await self.mass.streams.resolve_stream_url(
queue_item=next_item,
player_id=dlna_player.udn,
- content_type=ContentType.FLAC,
+ content_type=ContentType(output_codec),
# DLNA pre-caches pretty aggressively so do not yet start the runner
auto_start_runner=False,
)
from soco.events_base import SubscriptionBase
from soco.groups import ZoneGroup
-from music_assistant.common.models.config_entries import ConfigEntry
+from music_assistant.common.models.config_entries import CONF_ENTRY_OUTPUT_CODEC, ConfigEntry
from music_assistant.common.models.enums import (
ContentType,
MediaType,
PlayerFeature.VOLUME_MUTE,
PlayerFeature.VOLUME_SET,
)
-PLAYER_CONFIG_ENTRIES = tuple() # we don't have any player config entries (for now)
+PLAYER_CONFIG_ENTRIES = (CONF_ENTRY_OUTPUT_CODEC,)
async def setup(
for player in self.sonosplayers.values():
player.soco.end_direct_control_session
+ def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: # noqa: ARG002
+ """Return all (provider/player specific) Config Entries for the given player (if any)."""
+ return PLAYER_CONFIG_ENTRIES
+
def on_player_config_changed(
self, config: PlayerConfig, changed_keys: set[str] # noqa: ARG002
) -> None:
--- /dev/null
+"""
+Helper to trace memory usage.
+
+https://www.red-gate.com/simple-talk/development/python/memory-profiling-in-python-with-tracemalloc/
+"""
+import asyncio
+import tracemalloc
+
+# ruff: noqa: D103,E501,E741
+
+# list to store memory snapshots
+snaps = []
+
+
+def _take_snapshot():
+ snaps.append(tracemalloc.take_snapshot())
+
+
+async def take_snapshot():
+ loop = asyncio.get_running_loop()
+ await loop.run_in_executor(None, _take_snapshot)
+
+
+def _display_stats():
+ stats = snaps[0].statistics("filename")
+ print("\n*** top 5 stats grouped by filename ***")
+ for s in stats[:5]:
+ print(s)
+
+
+async def display_stats():
+ loop = asyncio.get_running_loop()
+ await loop.run_in_executor(None, _display_stats)
+
+
+def compare():
+ first = snaps[0]
+ for snapshot in snaps[1:]:
+ stats = snapshot.compare_to(first, "lineno")
+ print("\n*** top 10 stats ***")
+ for s in stats[:10]:
+ print(s)
+
+
+def print_trace():
+ # pick the last saved snapshot, filter noise
+ snapshot = snaps[-1].filter_traces(
+ (
+ tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
+ tracemalloc.Filter(False, "<frozen importlib._bootstrap_external>"),
+ tracemalloc.Filter(False, "<unknown>"),
+ )
+ )
+ largest = snapshot.statistics("traceback")[0]
+
+ print(
+ f"\n*** Trace for largest memory block - ({largest.count} blocks, {largest.size/1024} Kb) ***"
+ )
+ for l in largest.traceback.format():
+ print(l)