Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various fixed and optimizations #604

Merged
merged 9 commits into from
Apr 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ docker run --network host --privileged ghcr.io/music-assistant/server

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).

____________

Expand Down
3 changes: 2 additions & 1 deletion music_assistant/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def main():
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)
Expand All @@ -100,7 +101,7 @@ def on_shutdown(loop):

async def start_mass():
loop = asyncio.get_running_loop()
if log_level == "DEBUG":
if dev_mode:
loop.set_debug(True)
await mass.start()

Expand Down
19 changes: 19 additions & 0 deletions music_assistant/common/models/config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
CONF_FLOW_MODE,
CONF_LOG_LEVEL,
CONF_OUTPUT_CHANNELS,
CONF_OUTPUT_CODEC,
CONF_VOLUME_NORMALISATION,
CONF_VOLUME_NORMALISATION_TARGET,
SECURE_STRING_SUBSTITUTE,
Expand Down Expand Up @@ -367,3 +368,21 @@ class ConfigUpdate(DataClassDictMixin):
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,
)
1 change: 1 addition & 0 deletions music_assistant/common/models/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ class PlayerFeature(StrEnum):
SEEK = "seek"
SET_MEMBERS = "set_members"
QUEUE = "queue"
CROSSFADE = "crossfade"


class EventType(StrEnum):
Expand Down
1 change: 1 addition & 0 deletions music_assistant/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
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"
Expand Down
17 changes: 15 additions & 2 deletions music_assistant/server/controllers/media/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,20 @@ async def get_provider_item(
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(
Expand Down Expand Up @@ -449,7 +462,7 @@ async def dynamic_tracks(
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
Expand Down
22 changes: 15 additions & 7 deletions music_assistant/server/controllers/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,17 +643,25 @@ def _get_player_ffmpeg_args(
"-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
Expand Down
3 changes: 2 additions & 1 deletion music_assistant/server/helpers/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ async def write(self, data: bytes) -> None:
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."""
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
19 changes: 15 additions & 4 deletions music_assistant/server/providers/chromecast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
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,
Expand All @@ -31,7 +35,12 @@
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
Expand All @@ -49,6 +58,7 @@

CONF_ALT_APP = "alt_app"


BASE_PLAYER_CONFIG_ENTRIES = (
ConfigEntry(
key=CONF_ALT_APP,
Expand All @@ -59,6 +69,7 @@
"the playback experience but may not work on non-Google hardware.",
advanced=True,
),
CONF_ENTRY_OUTPUT_CODEC,
)


Expand Down Expand Up @@ -188,13 +199,13 @@ async def cmd_play_media(
) -> 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
Expand Down
19 changes: 13 additions & 6 deletions music_assistant/server/providers/dlna/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@
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

Expand All @@ -46,7 +46,7 @@
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")
Expand Down Expand Up @@ -220,6 +220,10 @@ async def unload(self) -> None:
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:
Expand Down Expand Up @@ -258,13 +262,13 @@ async def cmd_play_media(
# 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,
)

Expand Down Expand Up @@ -548,10 +552,13 @@ async def _enqueue_next_track(
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,
)
Expand Down
8 changes: 6 additions & 2 deletions music_assistant/server/providers/sonos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
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,
Expand Down Expand Up @@ -42,7 +42,7 @@
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(
Expand Down Expand Up @@ -227,6 +227,10 @@ async def unload(self) -> None:
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:
Expand Down
60 changes: 60 additions & 0 deletions script/profiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
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)