"""Store (EBU-R128) Integrated Loudness Measurement for a mediaitem in db."""
if not (provider := self.mass.get_provider(provider_instance_id_or_domain)):
return
+ if loudness in (None, inf, -inf):
+ # skip invalid values
+ return
values = {
"item_id": item_id,
"media_type": media_type.value,
"provider": provider.lookup_key,
"loudness": loudness,
}
- if album_loudness is not None:
+ if album_loudness not in (None, inf, -inf):
values["loudness_album"] = album_loudness
await self.database.insert_or_replace(DB_TABLE_LOUDNESS_MEASUREMENTS, values)
item_id: str,
provider_instance_id_or_domain: str,
media_type: MediaType = MediaType.TRACK,
- ) -> tuple[float, float] | None:
+ ) -> tuple[float, float | None] | None:
"""Get (EBU-R128) Integrated Loudness Measurement for a mediaitem in db."""
if not (provider := self.mass.get_provider(provider_instance_id_or_domain)):
return None
},
)
if db_row and db_row["loudness"] != inf and db_row["loudness"] != -inf:
- return (db_row["loudness"], db_row["loudness_album"])
+ loudness = db_row["loudness"]
+ loudness_album = db_row["loudness_album"]
+ if loudness_album in (inf, -inf):
+ loudness_album = None
+ return (loudness, loudness_album)
return None
ContentType,
EventType,
MediaType,
+ PlayerFeature,
PlayerState,
ProviderFeature,
QueueOption,
if queue.state == PlayerState.PLAYING:
queue.resume_pos = queue.corrected_elapsed_time
# forward the actual command to the player controller
- await self.mass.players.cmd_pause(queue_id)
+ queue_player = self.mass.players.get(queue_id)
+ if not (player_provider := self.mass.players.get_player_provider(queue.queue_id)):
+ return # guard
+
+ if PlayerFeature.PAUSE not in queue_player.supported_features:
+ # if player does not support pause, we need to send stop
+ await player_provider.cmd_stop(queue_player.player_id)
+ return
+ await player_provider.cmd_pause(queue_player.player_id)
+
+ async def _watch_pause() -> None:
+ count = 0
+ # wait for pause
+ while count < 5 and queue_player.state == PlayerState.PLAYING:
+ count += 1
+ await asyncio.sleep(1)
+ # wait for unpause
+ if queue_player.state != PlayerState.PAUSED:
+ return
+ count = 0
+ while count < 30 and queue_player.state == PlayerState.PAUSED:
+ count += 1
+ await asyncio.sleep(1)
+ # if player is still paused when the limit is reached, send stop
+ if queue_player.state == PlayerState.PAUSED:
+ await player_provider.cmd_stop(queue_player.player_id)
+
+ # we auto stop a player from paused when its paused for 30 seconds
+ if not queue_player.announcement_in_progress:
+ self.mass.create_task(_watch_pause())
@api_command("player_queues/play_pause")
async def play_pause(self, queue_id: str) -> None:
# resume requested while already playing,
# use current position as resume position
resume_pos = queue.corrected_elapsed_time
+ fade_in = False
else:
resume_pos = queue.resume_pos or queue.elapsed_time
resume_pos = 0
if resume_item is not None:
- resume_pos = resume_pos if resume_pos > 10 else 0
queue_player = self.mass.players.get(queue_id)
- if fade_in is None and queue_player.state == PlayerState.IDLE:
+ if (
+ fade_in is None
+ and queue_player.state == PlayerState.IDLE
+ and (time.time() - queue.elapsed_time_last_updated) > 60
+ ):
+ # enable fade in effect if the player is idle for a while
fade_in = resume_pos > 0
if resume_item.media_type == MediaType.RADIO:
# we're not able to skip in online radio so this is pointless
- player_id: player_id of the player to handle the command.
"""
player = self._get_player_with_redirect(player_id)
+ # Redirect to queue controller if it is active
+ if active_queue := self.mass.player_queues.get(player.active_source):
+ await self.mass.player_queues.pause(active_queue.queue_id)
+ return
if PlayerFeature.PAUSE not in player.supported_features:
# if player does not support pause, we need to send stop
- self.logger.info(
+ self.logger.debug(
"Player %s does not support pause, using STOP instead",
player.display_name,
)
player_provider = self.get_player_provider(player.player_id)
await player_provider.cmd_pause(player.player_id)
- async def _watch_pause(_player_id: str) -> None:
- player = self.get(_player_id, True)
- count = 0
- # wait for pause
- while count < 5 and player.state == PlayerState.PLAYING:
- count += 1
- await asyncio.sleep(1)
- # wait for unpause
- if player.state != PlayerState.PAUSED:
- return
- count = 0
- while count < 30 and player.state == PlayerState.PAUSED:
- count += 1
- await asyncio.sleep(1)
- # if player is still paused when the limit is reached, send stop
- if player.state == PlayerState.PAUSED:
- await self.cmd_stop(_player_id)
-
- # we auto stop a player from paused when its paused for 30 seconds
- if not player.announcement_in_progress:
- self.mass.create_task(_watch_pause(player_id))
-
@api_command("players/cmd/play_pause")
async def cmd_play_pause(self, player_id: str) -> None:
"""Toggle play/pause on given player.
elif not player_disabled and resume_queue and resume_queue.state == PlayerState.PLAYING:
# always stop first to ensure the player uses the new config
await self.mass.player_queues.stop(resume_queue.queue_id)
- self.mass.call_later(1, self.mass.player_queues.resume, resume_queue.queue_id)
+ self.mass.call_later(1, self.mass.player_queues.resume, resume_queue.queue_id, False)
# check for group memberships that need to be updated
if player_disabled and player.active_group and player_provider:
# try to remove from the group
if player.state == PlayerState.PLAYING:
self.logger.info("Restarting playback of Player %s after DSP change", player_id)
# this will restart ffmpeg with the new settings
- self.mass.call_later(0, self.mass.player_queues.resume, player.active_source)
+ self.mass.call_later(0, self.mass.player_queues.resume, player.active_source, False)
def _get_player_with_redirect(self, player_id: str) -> Player:
"""Get player with check if playback related command should be redirected."""
import urllib.parse
from collections.abc import AsyncGenerator
from dataclasses import dataclass, field
-from math import inf
from typing import TYPE_CHECKING
from aiofiles.os import wrap
elif streamdetails.volume_normalization_mode == VolumeNormalizationMode.MEASUREMENT_ONLY:
# volume normalization with known loudness measurement
# apply volume/gain correction
- if streamdetails.prefer_album_loudness and streamdetails.loudness_album not in (
- inf,
- -inf,
- ):
+ if streamdetails.prefer_album_loudness and streamdetails.loudness_album is not None:
gain_correct = streamdetails.target_loudness - streamdetails.loudness_album
else:
gain_correct = streamdetails.target_loudness - streamdetails.loudness
elif streamdetails.stream_type == StreamType.CUSTOM:
# prefer cache for custom streams (to speedup seeking)
max_filesize = 250 * 1024 * 1024 # 250MB
- return get_chunksize(streamdetails.audio_format, streamdetails.duration) < max_filesize
elif streamdetails.stream_type == StreamType.HLS:
# prefer cache for HLS streams (to speedup seeking)
max_filesize = 250 * 1024 * 1024 # 250MB
+ elif streamdetails.media_type in (
+ MediaType.AUDIOBOOK,
+ MediaType.PODCAST_EPISODE,
+ ):
+ # prefer cache for audiobooks and episodes (to speedup seeking)
+ max_filesize = 2 * 1024 * 1024 * 1024 # 2GB
elif streamdetails.provider in SLOW_PROVIDERS:
# prefer cache for slow providers
- max_filesize = 500 * 1024 * 1024 # 500MB
+ max_filesize = 2 * 1024 * 1024 * 1024 # 2GB
else:
- max_filesize = 25 * 1024 * 1024
+ max_filesize = 50 * 1024 * 1024
return estimated_filesize < max_filesize
lost_packets += 1
if lost_packets == 100:
logger.error("High packet loss detected, restarting playback...")
- self.mass.create_task(self.mass.player_queues.resume(queue.queue_id))
+ self.mass.create_task(self.mass.player_queues.resume(queue.queue_id, False))
else:
logger.warning("Packet loss detected!")
if "end of stream reached" in line:
player_provider = self.mass.players.get_player_provider(child_player.player_id)
if group_type == GROUP_TYPE_UNIVERSAL:
if was_playing:
- # stop playing the group player
+ # stop playing the child player that was unjoined from the UGP
await player_provider.cmd_stop(child_player.player_id)
self._update_attributes(group_player)
return
if child_player.group_childs:
# this is the sync leader, unsync all its childs!
# NOTE that some players/providers might support this in a less intrusive way
- # but for now we just ungroup all childs to keep thinngs universal
+ # but for now we just ungroup all childs to keep things universal
self.logger.info("Detected ungroup of sync leader, ungrouping all childs")
async with TaskManager(self.mass) as tg:
for sync_child_id in child_player.group_childs:
changed = True
if changed and player.state == PlayerState.PLAYING:
# Restart playback to ensure all members play the same content
- await self.mass.player_queues.resume(player.player_id)
+ await self.mass.player_queues.resume(player.player_id, False)
async def _serve_ugp_stream(self, request: web.Request) -> web.Response:
"""Serve the UGP (multi-client) flow stream audio to a player."""
StreamType,
)
from music_assistant_models.errors import InvalidProviderURI, MediaNotFoundError
-from music_assistant_models.media_items import (
- AudioFormat,
- Podcast,
- PodcastEpisode,
-)
+from music_assistant_models.media_items import AudioFormat, Podcast, PodcastEpisode
from music_assistant_models.streamdetails import StreamDetails
from music_assistant.helpers.compare import create_safe_string
-from music_assistant.helpers.podcast_parsers import (
- parse_podcast,
- parse_podcast_episode,
-)
+from music_assistant.helpers.podcast_parsers import parse_podcast, parse_podcast_episode
from music_assistant.models.music_provider import MusicProvider
if TYPE_CHECKING:
"""List all episodes for the podcast."""
if prov_podcast_id != self.podcast_id:
raise Exception(f"Podcast id not in provider: {prov_podcast_id}")
- for idx, episode in enumerate(self.parsed_podcast["episodes"]):
+ # sort episodes by published date
+ episodes: list[dict[str, Any]] = self.parsed_podcast["episodes"]
+ if episodes and episodes[0].get("published", 0) != 0:
+ episodes.sort(key=lambda x: x.get("published", 0))
+ for idx, episode in enumerate(episodes):
yield await self._parse_episode(episode, idx)
async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails: