from music_assistant.constants import (
ANNOUNCE_ALERT_FILE,
- CONF_ALLOW_AUDIO_CACHE,
CONF_BIND_IP,
CONF_BIND_PORT,
CONF_CROSSFADE_DURATION,
VERBOSE_LOG_LEVEL,
)
from music_assistant.controllers.players.player_controller import AnnounceData
+from music_assistant.helpers.audio import LOGGER as AUDIO_LOGGER
from music_assistant.helpers.audio import (
- CACHE_FILES_IN_USE,
get_chunksize,
get_media_stream,
get_player_filter_params,
get_stream_details,
resample_pcm_audio,
)
-from music_assistant.helpers.audio import LOGGER as AUDIO_LOGGER
from music_assistant.helpers.ffmpeg import LOGGER as FFMPEG_LOGGER
from music_assistant.helpers.ffmpeg import check_ffmpeg_version, get_ffmpeg_stream
from music_assistant.helpers.smart_fades import (
SmartFadesMixer,
SmartFadesMode,
)
-from music_assistant.helpers.util import (
- get_folder_size,
- get_free_space,
- get_free_space_percentage,
- get_ip_addresses,
- select_free_port,
-)
+from music_assistant.helpers.util import get_ip_addresses, select_free_port
from music_assistant.helpers.webserver import Webserver
from music_assistant.models.core_controller import CoreController
from music_assistant.models.plugin import PluginProvider
)
self.manifest.icon = "cast-audio"
self.announcements: dict[str, AnnounceData] = {}
- # prefer /tmp/.audio as audio cache dir
- self._audio_cache_dir = os.path.join("/tmp/.audio") # noqa: S108
- self.allow_cache_default = "auto"
self._crossfade_data: dict[str, CrossfadeData] = {}
self._bind_ip: str = "0.0.0.0"
self._smart_fades_mixer = SmartFadesMixer(self.mass)
"""Return the IP address this streamserver is bound to."""
return self._bind_ip
- @property
- def audio_cache_dir(self) -> str:
- """Return the directory where (temporary) audio cache files are stored."""
- return self._audio_cache_dir
-
async def get_config_entries(
self,
action: str | None = None,
category="advanced",
required=False,
),
- ConfigEntry(
- key=CONF_ALLOW_AUDIO_CACHE,
- type=ConfigEntryType.STRING,
- default_value=self.allow_cache_default,
- options=[
- ConfigValueOption("Always", "always"),
- ConfigValueOption("Disabled", "disabled"),
- ConfigValueOption("Auto", "auto"),
- ],
- label="Allow caching of remote/cloudbased audio streams",
- description="To ensure smooth(er) playback as well as fast seeking, "
- "Music Assistant can cache audio streams on disk. \n"
- "On systems with limited diskspace, this can be disabled, "
- "but may result in less smooth playback or slower seeking.\n\n"
- "**Always:** Enforce caching of audio streams at all times "
- "(as long as there is enough free space).\n"
- "**Disabled:** Never cache audio streams.\n"
- "**Auto:** Let Music Assistant decide if caching "
- "should be used on a per-item base.",
- category="advanced",
- required=True,
- ),
+ # ConfigEntry(
+ # key=CONF_ALLOW_AUDIO_CACHE,
+ # type=ConfigEntryType.STRING,
+ # default_value=self.allow_cache_default,
+ # options=[
+ # ConfigValueOption("Always", "always"),
+ # ConfigValueOption("Disabled", "disabled"),
+ # ConfigValueOption("Auto", "auto"),
+ # ],
+ # label="Allow caching of remote/cloudbased audio streams",
+ # description="To ensure smooth(er) playback as well as fast seeking, "
+ # "Music Assistant can cache audio streams on disk. \n"
+ # "On systems with limited diskspace, this can be disabled, "
+ # "but may result in less smooth playback or slower seeking.\n\n"
+ # "**Always:** Enforce caching of audio streams at all times "
+ # "(as long as there is enough free space).\n"
+ # "**Disabled:** Never cache audio streams.\n"
+ # "**Auto:** Let Music Assistant decide if caching "
+ # "should be used on a per-item base.",
+ # category="advanced",
+ # required=True,
+ # ),
)
async def setup(self, config: CoreConfig) -> None:
FFMPEG_LOGGER.setLevel(self.logger.level)
# perform check for ffmpeg version
await check_ffmpeg_version()
- if self.mass.running_as_hass_addon:
- # When running as HAOS add-on, we run /tmp as tmpfs so we need
- # to pick another temporary location which is not /tmp.
- # We prefer the root/user dir because it will be cleaned up on a reboot
- self._audio_cache_dir = os.path.join(os.path.expanduser("~"), ".audio")
- if not await asyncio.to_thread(os.path.isdir, self._audio_cache_dir):
- await asyncio.to_thread(os.makedirs, self._audio_cache_dir)
- # enable cache by default if we have enough free space only
- disk_percentage_free = await get_free_space_percentage(self._audio_cache_dir)
- self.allow_cache_default = "auto" if disk_percentage_free > 25 else "disabled"
- # schedule cleanup of old audio cache files
- await self._clean_audio_cache()
# start the webserver
self.publish_port = config.get_value(CONF_BIND_PORT)
self.publish_ip = config.get_value(CONF_PUBLISH_IP)
filter_params.append(f"volume={gain_correct}dB")
streamdetails.volume_normalization_gain_correct = gain_correct
- if streamdetails.media_type == MediaType.RADIO or not streamdetails.duration:
- # pad some silence before the radio/live stream starts to create some headroom
- # for radio stations (or other live streams) that do not provide any look ahead buffer
- # without this, some radio streams jitter a lot, especially with dynamic normalization,
- # if the stream does not provide a look ahead buffer
- async for silence in get_silence(4, pcm_format):
- yield silence
- del silence
+ # if streamdetails.media_type == MediaType.RADIO or not streamdetails.duration:
+ # # pad some silence before the radio/live stream starts to create some headroom
+ # # for radio stations (or other live streams) that do not provide any look ahead buffer
+ # # without this, some radio streams jitter a lot,
+ # # especially with dynamic normalization,
+ # # if the stream does not provide a look ahead buffer
+ # async for silence in get_silence(4, pcm_format):
+ # yield silence
+ # del silence
first_chunk_received = False
async for chunk in get_media_stream(
channels=2,
)
- async def _clean_audio_cache(self) -> None:
- """Clean up audio cache periodically."""
- free_space_in_cache_dir = await get_free_space(self._audio_cache_dir)
- # calculate max cache size based on free space in cache dir
- max_cache_size = min(15, free_space_in_cache_dir * 0.2)
- cache_enabled = await self.mass.config.get_core_config_value(
- self.domain, CONF_ALLOW_AUDIO_CACHE
- )
- if cache_enabled == "disabled":
- max_cache_size = 0.001
-
- def _clean_old_files(foldersize: float) -> None:
- files: list[os.DirEntry] = [x for x in os.scandir(self._audio_cache_dir) if x.is_file()]
- files.sort(key=lambda x: x.stat().st_atime)
- for _file in files:
- if _file.path in CACHE_FILES_IN_USE:
- continue
- foldersize -= _file.stat().st_size / float(1 << 30)
- os.remove(_file.path)
- if foldersize < max_cache_size:
- return
-
- foldersize = await get_folder_size(self._audio_cache_dir)
- if foldersize > max_cache_size:
- await asyncio.to_thread(_clean_old_files, foldersize)
- # reschedule self
- self.mass.call_later(3600, self._clean_audio_cache)
-
def _crossfade_allowed(
self, queue_item: QueueItem, smart_fades_mode: SmartFadesMode, flow_mode: bool = False
) -> bool: